Skip to content

fix: Race condition for app launch profiling #5300

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

philipphofmann
Copy link
Member

📜 Description

When having app launch profiling and TTFD enabled, a race condition when reading alwaysWaitForFullDisplay could occur. This is fixed now by reading if TTFD is enabled from the options instead of the UIViewControllerPerformanceTracker.

💡 Motivation and Context

Running the tests with the thread sanitizer enabled surfaced this data race.

==================
WARNING: ThreadSanitizer: data race (pid=47792)
  Read of size 1 at 0x00030aa799f8 by thread T350:
    #0 -[SentryUIViewControllerPerformanceTracker alwaysWaitForFullDisplay] <null> (Sentry:arm64+0x18c298)
    #1 __sentry_sdkInitProfilerTasks_block_invoke <null> (Sentry:arm64+0x12fca8)
    #2 __53-[SentryDispatchQueueWrapper dispatchAsyncWithBlock:]_block_invoke <null> (Sentry:arm64+0x144bc)
    #3 __tsan::invoke_and_release_block(void*) <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7f97c)
    #4 _dispatch_client_callout <null> (libdispatch.dylib:arm64+0x1c274)

  Previous write of size 1 at 0x00030aa799f8 by main thread (mutexes: write M0):
    #0 -[SentryUIViewControllerPerformanceTracker initWithTracker:dispatchQueueWrapper:] <null> (Sentry:arm64+0x1860bc)
    #1 -[SentryDependencyContainer uiViewControllerPerformanceTracker] <null> (Sentry:arm64+0x178bc8)
    #2 SentryTests.SentrySDKTests.testReportFullyDisplayed() -> () <null> (SentryTests:arm64+0x783388)
    #3 @objc SentryTests.SentrySDKTests.testReportFullyDisplayed() -> () <null> (SentryTests:arm64+0x783728)
    #4 __invoking___ <null> (CoreFoundation:arm64+0x13a20c)

  Location is heap block of size 80 at 0x00030aa799f0 allocated by main thread:
    #0 calloc <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x5ed24)
    #1 _malloc_type_calloc_outlined <null> (libsystem_malloc.dylib:arm64+0xf308)
    #2 SentryTests.SentrySDKTests.testReportFullyDisplayed() -> () <null> (SentryTests:arm64+0x783388)
    #3 @objc SentryTests.SentrySDKTests.testReportFullyDisplayed() -> () <null> (SentryTests:arm64+0x783728)
    #4 __invoking___ <null> (CoreFoundation:arm64+0x13a20c)

  Mutex M0 (0x000109f0eb60) created at:
    #0 objc_sync_enter <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x7d384)
    #1 -[SentryDependencyContainer crashReporter] <null> (Sentry:arm64+0x178094)
    #2 __32-[SentryCrashWrapper systemInfo]_block_invoke <null> (Sentry:arm64+0x4e73c)
    #3 dispatch_once <null> (libclang_rt.tsan_iossim_dynamic.dylib:arm64+0x80988)
    #4 -[SentryCrashWrapper systemInfo] <null> (Sentry:arm64+0x4e660)
    #5 -[SentryCrashWrapper enrichScope:] <null> (Sentry:arm64+0x4ec28)
    #6 -[SentryHub initWithClient:andScope:andCrashWrapper:andDispatchQueue:] <null> (Sentry:arm64+0xaf850)
    #7 -[SentryHub initWithClient:andScope:] <null> (Sentry:arm64+0xaf2cc)
    #8 __30+[SentrySDK startWithOptions:]_block_invoke <null> (Sentry:arm64+0x1427a8)
    #9 -[SentryDispatchQueueWrapper dispatchAsyncOnMainQueue:] <null> (Sentry:arm64+0x14638)
    #10 +[SentrySDK startWithOptions:] <null> (Sentry:arm64+0x1424e0)
    #11 +[SentrySDK startWithConfigureOptions:] <null> (Sentry:arm64+0x142ba8)
    #12 SentryTests.DataSentryTracingIntegrationTests.(Fixture in _F9C28CBE30EAC9BE15107FEF3A455A1A).getSut(testName: Swift.String, isSDKEnabled: Swift.Bool, isEnabled: Swift.Bool) throws -> Foundation.Data <null> (SentryTests:arm64+0xb4f298)
    #13 SentryTests.DataSentryTracingIntegrationTests.testInitContentsOfWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws -> () <null> (SentryTests:arm64+0xb5f9f4)
    #14 @objc SentryTests.DataSentryTracingIntegrationTests.testInitContentsOfWithSentryTracing_fileIsIgnored_shouldNotTraceManually() throws -> () <null> (SentryTests:arm64+0xb609bc)
    #15 __invoking___ <null> (CoreFoundation:arm64+0x13a20c)

  Thread T350 (tid=446262, running) is a GCD worker thread

SUMMARY: ThreadSanitizer: data race (/Users/philipp.hofmann/Library/Developer/Xcode/DerivedData/Sentry-bahizrqpvqbrnycjvclkuwoibtgv/Build/Products/Test-iphonesimulator/Sentry.framework/Sentry:arm64+0x18c298) in -[SentryUIViewControllerPerformanceTracker alwaysWaitForFullDisplay]+0x40
==================

The problem is that the SentryPerformanceTrackingIntegration writes alwaysWaitForFullDisplay on the main thread here

SentryUIViewControllerPerformanceTracker *performanceTracker =
[SentryDependencyContainer.sharedInstance uiViewControllerPerformanceTracker];
performanceTracker.alwaysWaitForFullDisplay = options.enableTimeToFullDisplayTracing;

While the SentryProfiler reads the same value on a BG thread here

[SentryDependencyContainer.sharedInstance.dispatchQueueWrapper dispatchAsyncWithBlock:^{
// get the configuration options from the last time the launch config was written; it may be
// different than the new options the SDK was just started with
const auto configDict = sentry_appLaunchProfileConfiguration();
const auto profileIsContinuousV1 =
[configDict[kSentryLaunchProfileConfigKeyContinuousProfiling] boolValue];
const auto profileIsContinuousV2 =
[configDict[kSentryLaunchProfileConfigKeyContinuousProfilingV2] boolValue];
const auto v2LifecycleValue
= configDict[kSentryLaunchProfileConfigKeyContinuousProfilingV2Lifecycle];
const auto v2Lifecycle = (SentryProfileLifecycle)
[configDict[kSentryLaunchProfileConfigKeyContinuousProfilingV2Lifecycle] intValue];
const auto v2LifecycleIsManual = profileIsContinuousV2 && v2LifecycleValue != nil
&& v2Lifecycle == SentryProfileLifecycleManual;
BOOL shouldStopAndTransmitLaunchProfile = YES;
# if SENTRY_HAS_UIKIT
const auto v2LifecycleIsTrace = profileIsContinuousV2 && v2LifecycleValue != nil
&& v2Lifecycle == SentryProfileLifecycleTrace;
const auto profileIsCorrelatedToTrace = !profileIsContinuousV2 || v2LifecycleIsTrace;
SentryUIViewControllerPerformanceTracker *performanceTracker =
[SentryDependencyContainer.sharedInstance uiViewControllerPerformanceTracker];
if (profileIsCorrelatedToTrace && performanceTracker.alwaysWaitForFullDisplay) {
SENTRY_LOG_DEBUG(@"Will wait to stop launch profile correlated to a trace until full "
@"display reported.");
shouldStopAndTransmitLaunchProfile = NO;
}
# endif // SENTRY_HAS_UIKIT

💚 How did you test it?

Running the tests again with the thread sanitizer enabled didn't surface this data race anymore. Ideally, we would write an integration test for this, but that is quite complicated. Enabling the thread sanitizer again in CI will surface such issues again.

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

When having app launch profiling and TTFD enabled, a race condition when
reading alwaysWaitForFullDisplay could occur. This is fixed now by
reading if TTFD is enabled from the options instead of the
UIViewControllerPerformanceTracker.
Copy link

codecov bot commented May 27, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.811%. Comparing base (2334b81) to head (91300b6).

Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##              main     #5300       +/-   ##
=============================================
+ Coverage   92.773%   92.811%   +0.037%     
=============================================
  Files          690       691        +1     
  Lines        86560     86710      +150     
  Branches     30009     30117      +108     
=============================================
+ Hits         80305     80477      +172     
+ Misses        6158      6139       -19     
+ Partials        97        94        -3     
Files with missing lines Coverage Δ
Sources/Sentry/SentryProfiler.mm 90.184% <100.000%> (+1.699%) ⬆️
...yProfilerTests/SentryAppLaunchProfilingTests.swift 100.000% <100.000%> (ø)

... and 28 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2334b81...91300b6. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

github-actions bot commented May 27, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1233.15 ms 1257.88 ms 24.73 ms
Size 23.76 KiB 821.30 KiB 797.54 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
adcc7d8 1225.90 ms 1245.08 ms 19.18 ms
dacf894 1232.32 ms 1236.34 ms 4.02 ms
3437454 1254.04 ms 1259.50 ms 5.46 ms
12e65d0 1232.17 ms 1265.39 ms 33.22 ms
9f0d9e0 1206.55 ms 1219.41 ms 12.86 ms
963b49c 1244.94 ms 1256.55 ms 11.61 ms
2283356 1228.77 ms 1248.47 ms 19.70 ms
e5dcbd5 1223.47 ms 1243.90 ms 20.43 ms
d3abae0 1200.36 ms 1224.22 ms 23.87 ms
83887af 1196.94 ms 1206.82 ms 9.88 ms

App size

Revision Plain With Sentry Diff
adcc7d8 20.76 KiB 426.15 KiB 405.39 KiB
dacf894 20.76 KiB 426.34 KiB 405.58 KiB
3437454 22.85 KiB 408.87 KiB 386.02 KiB
12e65d0 22.30 KiB 756.53 KiB 734.23 KiB
9f0d9e0 21.58 KiB 424.28 KiB 402.70 KiB
963b49c 22.30 KiB 749.83 KiB 727.53 KiB
2283356 23.76 KiB 820.30 KiB 796.54 KiB
e5dcbd5 22.85 KiB 414.09 KiB 391.24 KiB
d3abae0 20.76 KiB 434.92 KiB 414.16 KiB
83887af 21.58 KiB 419.64 KiB 398.06 KiB

Previous results on branch: fix/race-condition-app-launch-profiling

Startup times

Revision Plain With Sentry Diff
14eb066 1237.76 ms 1255.47 ms 17.71 ms
3311429 1234.13 ms 1259.98 ms 25.85 ms
ea59633 1230.90 ms 1249.35 ms 18.45 ms

App size

Revision Plain With Sentry Diff
14eb066 23.76 KiB 821.39 KiB 797.63 KiB
3311429 23.76 KiB 821.38 KiB 797.62 KiB
ea59633 23.76 KiB 821.29 KiB 797.53 KiB

Copy link
Contributor

@philprime philprime left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for the detailed PR description explaining the issue.

Copy link
Contributor

@philprime philprime left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for the detailed PR description explaining the issue.

@philipphofmann
Copy link
Member Author

All the UI tests are failing. I need to double check if I actually broke something.

Copy link
Member

@armcknight armcknight left a comment

Choose a reason for hiding this comment

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

Thanks for catching this @philipphofmann . We just need to check a few more options for it to be complete.

@@ -31,6 +31,27 @@ extension SentryAppLaunchProfilingTests {
XCTAssertNil(sentry_launchTracer)
}

// TTFD only works when UIKit is available, so we only test this on iOS.
Copy link
Member

Choose a reason for hiding this comment

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

we should just change the #if os(iOS) || os(macOS) || targetEnvironment(macCatalyst) to this at the beginning/end of this file. copypasta on my part!

SentryUIViewControllerPerformanceTracker *performanceTracker =
[SentryDependencyContainer.sharedInstance uiViewControllerPerformanceTracker];
if (profileIsCorrelatedToTrace && performanceTracker.alwaysWaitForFullDisplay) {

if (profileIsCorrelatedToTrace && options.enableTimeToFullDisplayTracing) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we'll also need to check a few other options, like options.enableUIViewControllerTracing, since you could theoretically misconfigure the SDK like:

options.enableUIViewControllerTracing = false
options.enableTimeToFullDisplayTracing = true

Checking SentryUIViewControllerPerformanceTracker.alwaysWaitForFullDisplay would have implicitly included all the option combination validation. I don't know what other options would be applicable, maybe enableAutoPerformanceTracing and tracesSampleRate/tracesSampler?

Copy link
Member

Choose a reason for hiding this comment

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

This is actually why the UI tests are failing now. Since we automatically configure enableUIViewControllerTracing to true unless the launch flag "--disable-time-to-full-display-tracing" is set in SentrySDKWrapper/SentrySDKOverrides, changing this now caused the conditional branch to be taken here where it was not before.

@philipphofmann
Copy link
Member Author

Thanks for the review @armcknight. Due to the hotfix and some unexpected immediate family member care leave yesterday, I was unable to include your feedback yet. As I'm on PTO the whole next week, this PR will lie around for a bit. If this is important, feel free to pick it up and as @philprime for another review. I can also tackle it once I'm back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants