Skip to content

Conversation

@adinauer
Copy link
Member

📜 Description

When creating (non empty) SentryOptions we previously eagerly created SentryExecutorService which in turn creates a thread named SentryExecutorServiceThreadFactory-0.

There were cases where we created options that ended up unused but we failed to clean those up, e.g.:

  • logging integrations that created SentryOptions and due to lower priority of the call to Sentry.init ended up unused
  • SentryProperties (a subclass of SentryOptions) were created even if the SDK wasn't enabled but we never cleaned up because the SDK was never enabled

💡 Motivation and Context

Fixes #5079

💚 How did you test it?

📝 Checklist

  • I added GH Issue ID & Linear ID
  • 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.

🔮 Next steps

@github-actions
Copy link
Contributor

github-actions bot commented Feb 11, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • (distribution) Add installGroupsOverride parameter by runningcode in #5066
  • (replay) Track custom masking usage via integration by markushi in #5070

Bug Fixes 🐛

  • (android) Merge tombstone and Native SDK event message. by supervacuus in #5095

Internal Changes 🔧

Deps

  • Bump getsentry/craft from 2.20.1 to 2.21.2 by dependabot in #5083
  • Bump github/codeql-action from 4.32.0 to 4.32.2 by dependabot in #5082
  • Update Native SDK to v0.12.6 by github-actions in #5071

Other

  • Fix thread leak caused by eager creation of SentryExecutorService in SentryOptions by adinauer in #5093

🤖 This preview updates automatically when you update the PR.

return;
}

final @NotNull SentryExecutorService startupExecutorService = new SentryExecutorService();
Copy link
Member Author

Choose a reason for hiding this comment

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

This creates a new thread for app startup profiling. My current understanding is that this is later reused and not just used for app startup profiling. This means we're not really leaking it since it ends up being used.

If there's cases where app startup profiler is abandoned, we'll need some sort of shutdown hook to also shut down the SentryExecutorService. LMK if I should implement that as well just to be safe here.

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 it's fine as-is. Plus, afaik we'll need to deprecate/delete the transaction profiler soon (with the next major likely or span-only), so I wouldn't pour much into it

Copy link
Member

Choose a reason for hiding this comment

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

ah I see this also affects continuous profiler too.. but I don't think we abandon it anywhere (unless process termination), so we hsould be good.

Btw, speaking of transaction profiler - I guess we should change it to accept a lambda, too (line 216)?

Copy link
Member Author

Choose a reason for hiding this comment

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

ah I see this also affects continuous profiler too.. but I don't think we abandon it anywhere (unless process termination), so we hsould be good.

I'd assume once we get rid of transaction profiler, there wouldn't be much reason to abandon it.
I'll go through some things again to check for cases but I assume we can tackle those as a follow up since this isn't new behaviour but already existed previously.

Btw, speaking of transaction profiler - I guess we should change it to accept a lambda, too (line 216)?

Yeah, will change that too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops, I created the ctor but didn't use it. It's being used now.

Copy link
Member Author

Choose a reason for hiding this comment

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

We agreed to ignore the edge case of an abandoned app start profiler for now since it only happens when switching between continuous and transaction profiling or when switching from enabled app startup profiling to disabled. And it'll only be leaking a single thread for a single app run. With plans to remove transaction based profiling in the future, this will become even more of an edge case.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 11, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 346.87 ms 442.22 ms 95.35 ms
Size 1.58 MiB 2.28 MiB 718.79 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
ee747ae 415.92 ms 470.15 ms 54.23 ms
27d7cf8 397.90 ms 498.65 ms 100.75 ms
bbc35bb 324.88 ms 425.73 ms 100.85 ms
d15471f 307.28 ms 381.85 ms 74.57 ms
d364ace 384.53 ms 453.51 ms 68.98 ms
ee747ae 386.94 ms 431.43 ms 44.49 ms
539ca63 313.51 ms 355.43 ms 41.92 ms
ee747ae 400.46 ms 423.61 ms 23.15 ms
b3d8889 371.69 ms 432.96 ms 61.26 ms
3d205d0 352.15 ms 432.53 ms 80.38 ms

App size

Revision Plain With Sentry Diff
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
27d7cf8 1.58 MiB 2.12 MiB 549.42 KiB
bbc35bb 1.58 MiB 2.12 MiB 553.01 KiB
d15471f 1.58 MiB 2.13 MiB 559.54 KiB
d364ace 1.58 MiB 2.11 MiB 539.75 KiB
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
539ca63 1.58 MiB 2.12 MiB 551.41 KiB
ee747ae 1.58 MiB 2.10 MiB 530.95 KiB
b3d8889 1.58 MiB 2.10 MiB 535.06 KiB
3d205d0 1.58 MiB 2.10 MiB 532.97 KiB

Previous results on branch: fix/thread-leak-from-executor-service

Startup times

Revision Plain With Sentry Diff
54d4b6e 306.94 ms 356.04 ms 49.10 ms

App size

Revision Plain With Sentry Diff
54d4b6e 1.58 MiB 2.28 MiB 718.86 KiB

private volatile boolean isRunning = false;
protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();

public AndroidProfiler(
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you for the heads up @romtsn 🙇
As you said this seems more like an internal implementation breakage (which we should handle with the Android bump) and not a user facing thing.

Copy link
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

one point about transaction profiler, but lgtm otherwise!

Comment on lines 165 to 171
return;
}

final @NotNull SentryExecutorService startupExecutorService = new SentryExecutorService();
final @NotNull IContinuousProfiler appStartContinuousProfiler =
new AndroidContinuousProfiler(
buildInfoProvider,
Copy link

Choose a reason for hiding this comment

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

Bug: SentryPerformanceProvider eagerly creates SentryExecutorService instances at app startup, spawning threads before Sentry.init() is called and potentially causing thread leaks if profiling is disabled.
Severity: MEDIUM

Suggested Fix

Instead of creating new SentryExecutorService instances with new SentryExecutorService(), the profilers in SentryPerformanceProvider should obtain the executor service from SentryOptions. This can be achieved by passing a supplier that lazily gets the executor from the options once the SDK is initialized, ensuring thread creation is deferred until Sentry.init() and managed within the central SDK lifecycle.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java#L165-L171

Potential issue: In `SentryPerformanceProvider.java`, new `SentryExecutorService`
instances are created directly via `new SentryExecutorService()`. This constructor
immediately spawns a thread. Since `SentryPerformanceProvider` is a `ContentProvider`
that runs at app startup before `Sentry.init()`, these threads are created
unconditionally, bypassing the lazy initialization logic intended for the SDK. If app
start profiling is not enabled, these threads are created but never used, resulting in a
thread leak for the lifetime of the application process. Additionally, these executors
are created with `null` options, which prevents proper logging of rejected tasks.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Member Author

Choose a reason for hiding this comment

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

We accept this edge case for now.
#5093 (comment)

@adinauer adinauer merged commit b6cfb57 into main Feb 12, 2026
63 checks passed
@adinauer adinauer deleted the fix/thread-leak-from-executor-service branch February 12, 2026 12:25
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.

Sentry Spring Boot starter leaves daemon thread running on undeploy

3 participants