From ce9f30af90d72aa251cc75c39965312d5c24ee08 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 29 Jul 2026 17:56:26 +0200 Subject: [PATCH 1/9] added pause --- include/sentry.h | 9 +++++++++ src/sentry_app_hang_latch.c | 20 ++++++++++++++++++++ src/sentry_app_hang_latch.h | 3 +++ src/sentry_app_hang_monitor.c | 15 +++++++++++++++ tests/unit/test_app_hang.c | 29 +++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 6 files changed, 77 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index daac01a58..cfd282c8b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2829,6 +2829,15 @@ SENTRY_EXPERIMENTAL_API uint64_t sentry_options_get_app_hang_timeout( */ SENTRY_EXPERIMENTAL_API void sentry_app_hang_heartbeat(void); +/** + * Pauses app-hang detection without changing the watched thread. + * + * While paused, the watchdog does not capture app hangs. A subsequent heartbeat + * from the watched thread automatically resumes detection. Heartbeats from + * other threads continue to be ignored. + */ +SENTRY_EXPERIMENTAL_API void sentry_app_hang_pause(void); + /** * Type of the `before_send_metric` callback. * diff --git a/src/sentry_app_hang_latch.c b/src/sentry_app_hang_latch.c index 334e89f4f..3ff340479 100644 --- a/src/sentry_app_hang_latch.c +++ b/src/sentry_app_hang_latch.c @@ -54,6 +54,7 @@ sentry__app_hang_should_capture( static uint64_t g_target_tid = 0; static uint64_t g_last_heartbeat_ms = 0; static volatile long g_app_hang_active = 0; +static volatile long g_app_hang_paused = 0; void sentry__app_hang_set_active(bool active) @@ -67,6 +68,23 @@ sentry__app_hang_is_active(void) return sentry__atomic_fetch(&g_app_hang_active) != 0; } +void +sentry__app_hang_set_paused(bool paused) +{ + if (paused) { + // Clear the old timestamp before publishing the paused state, so a + // concurrent heartbeat cannot leave detection armed with stale data. + sentry__atomic_store_u64(&g_last_heartbeat_ms, 0); + } + sentry__atomic_store(&g_app_hang_paused, paused ? 1 : 0); +} + +bool +sentry__app_hang_is_paused(void) +{ + return sentry__atomic_fetch(&g_app_hang_paused) != 0; +} + uint64_t sentry__app_hang_current_tid(void) { @@ -97,6 +115,7 @@ sentry__app_hang_latch_reset(void) { sentry__atomic_store_u64(&g_target_tid, 0); sentry__atomic_store_u64(&g_last_heartbeat_ms, 0); + sentry__atomic_store(&g_app_hang_paused, 0); } void @@ -115,6 +134,7 @@ sentry_app_hang_heartbeat(void) } if (target == tid) { // ignore heartbeats from other threads + sentry__atomic_store(&g_app_hang_paused, 0); sentry__atomic_store_u64( &g_last_heartbeat_ms, sentry__monotonic_time()); } diff --git a/src/sentry_app_hang_latch.h b/src/sentry_app_hang_latch.h index 5d0123543..a3042327e 100644 --- a/src/sentry_app_hang_latch.h +++ b/src/sentry_app_hang_latch.h @@ -26,6 +26,9 @@ void sentry__app_hang_set_active(bool active); // Whether app-hang detection is currently armed. bool sentry__app_hang_is_active(void); +void sentry__app_hang_set_paused(bool paused); +bool sentry__app_hang_is_paused(void); + sentry_value_t sentry__app_hang_make_event( void **ips, size_t frame_count, uint64_t freeze_ms); diff --git a/src/sentry_app_hang_monitor.c b/src/sentry_app_hang_monitor.c index ef5f2d9cd..8add2b944 100644 --- a/src/sentry_app_hang_monitor.c +++ b/src/sentry_app_hang_monitor.c @@ -99,6 +99,9 @@ worker(void *arg) if (!sentry__app_hang_is_active()) { break; } + if (sentry__app_hang_is_paused()) { + continue; + } const sentry_app_hang_latch_t latch = sentry__app_hang_current_latch(); uint64_t now = sentry__monotonic_time(); @@ -108,6 +111,9 @@ worker(void *arg) if (!sentry__app_hang_is_active()) { break; } + if (sentry__app_hang_is_paused()) { + continue; + } // Only mark this freeze as fired when an event was actually // captured. A transient stackwalk failure (0 frames) must not // suppress retries while the thread remains stuck. @@ -181,3 +187,12 @@ sentry__app_hang_monitor_stop(void) } #endif // SENTRY_HAS_THREAD_STACKWALK + +void +sentry_app_hang_pause(void) +{ + if (!sentry__app_hang_is_active()) { + return; + } + sentry__app_hang_set_paused(true); +} diff --git a/tests/unit/test_app_hang.c b/tests/unit/test_app_hang.c index 25e85caf6..844f4b7c9 100644 --- a/tests/unit/test_app_hang.c +++ b/tests/unit/test_app_hang.c @@ -43,6 +43,35 @@ SENTRY_TEST(app_hang_latch) sentry__app_hang_set_active(false); } +SENTRY_TEST(app_hang_pause_resumes_on_heartbeat) +{ +#if !SENTRY_HAS_THREAD_STACKWALK + SKIP_TEST(); +#endif + sentry__app_hang_latch_reset(); + sentry__app_hang_set_active(true); + sentry_app_hang_heartbeat(); + + sentry_app_hang_latch_t l = sentry__app_hang_current_latch(); + uint64_t target = l.target_tid; + TEST_CHECK(target != 0); + + sentry_app_hang_pause(); + l = sentry__app_hang_current_latch(); + TEST_CHECK(sentry__app_hang_is_paused()); + TEST_CHECK(l.target_tid == target); + TEST_CHECK(l.last_heartbeat_ms == 0); + + sentry_app_hang_heartbeat(); + l = sentry__app_hang_current_latch(); + TEST_CHECK(!sentry__app_hang_is_paused()); + TEST_CHECK(l.target_tid == target); + TEST_CHECK(l.last_heartbeat_ms != 0); + + sentry__app_hang_latch_reset(); + sentry__app_hang_set_active(false); +} + SENTRY_TEST(app_hang_make_event) { void *ips[2] = { (void *)0x1000, (void *)0x2000 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 6789e1d92..9005748fd 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -3,6 +3,7 @@ XX(app_hang_end_to_end) XX(app_hang_latch) XX(app_hang_make_event) XX(app_hang_monitor_fires) +XX(app_hang_pause_resumes_on_heartbeat) XX(app_hang_should_capture) XX(assert_sdk_name) XX(assert_sdk_user_agent) From bae9102c898d68338a92db490e21e63c5fe9a93b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 29 Jul 2026 18:01:58 +0200 Subject: [PATCH 2/9] updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 488c3f9a5..4c6da6db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ ## Unreleased +**Features** + +- Added `sentry_app_hang_pause` to allow pausing of the app hang detection mechanism. This is useful in settings where an app can be put in the background, causing the monitored thread to suspend execution. ([#1928](https://github.com/getsentry/sentry-native/pull/1928)) + **Fixes**: + - Honor checks before launching crash reporter ([#1906](https://github.com/getsentry/sentry-native/pull/1906)) ## 0.16.0 From 3d15af9c3c4204c2659b4c1c02455c388246d12d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 29 Jul 2026 18:04:46 +0200 Subject: [PATCH 3/9] prevent stale heartbeat check --- src/sentry_app_hang_latch.c | 4 +++- tests/unit/test_app_hang.c | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sentry_app_hang_latch.c b/src/sentry_app_hang_latch.c index 3ff340479..c9dc113ab 100644 --- a/src/sentry_app_hang_latch.c +++ b/src/sentry_app_hang_latch.c @@ -134,8 +134,10 @@ sentry_app_hang_heartbeat(void) } if (target == tid) { // ignore heartbeats from other threads - sentry__atomic_store(&g_app_hang_paused, 0); sentry__atomic_store_u64( &g_last_heartbeat_ms, sentry__monotonic_time()); + // Publish the fresh timestamp before resuming so the watchdog cannot + // observe an unpaused detector paired with a stale heartbeat. + sentry__atomic_store(&g_app_hang_paused, 0); } } diff --git a/tests/unit/test_app_hang.c b/tests/unit/test_app_hang.c index 844f4b7c9..3d417867d 100644 --- a/tests/unit/test_app_hang.c +++ b/tests/unit/test_app_hang.c @@ -54,7 +54,9 @@ SENTRY_TEST(app_hang_pause_resumes_on_heartbeat) sentry_app_hang_latch_t l = sentry__app_hang_current_latch(); uint64_t target = l.target_tid; + uint64_t first_heartbeat = l.last_heartbeat_ms; TEST_CHECK(target != 0); + TEST_CHECK(first_heartbeat != 0); sentry_app_hang_pause(); l = sentry__app_hang_current_latch(); @@ -62,11 +64,12 @@ SENTRY_TEST(app_hang_pause_resumes_on_heartbeat) TEST_CHECK(l.target_tid == target); TEST_CHECK(l.last_heartbeat_ms == 0); + sleep_ms(1); sentry_app_hang_heartbeat(); l = sentry__app_hang_current_latch(); TEST_CHECK(!sentry__app_hang_is_paused()); TEST_CHECK(l.target_tid == target); - TEST_CHECK(l.last_heartbeat_ms != 0); + TEST_CHECK(l.last_heartbeat_ms > first_heartbeat); sentry__app_hang_latch_reset(); sentry__app_hang_set_active(false); From ffbd2174835b12f05e55bd80ce18f4f6e077243d Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Thu, 30 Jul 2026 12:24:26 +0200 Subject: [PATCH 4/9] Update CHANGELOG.md Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c6da6db9..f2cdb42c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -**Features** +**Features**: - Added `sentry_app_hang_pause` to allow pausing of the app hang detection mechanism. This is useful in settings where an app can be put in the background, causing the monitored thread to suspend execution. ([#1928](https://github.com/getsentry/sentry-native/pull/1928)) From bc7fc08166cf977d4afef94c373d8931e71c8938 Mon Sep 17 00:00:00 2001 From: Stefan Jandl Date: Thu, 30 Jul 2026 12:24:34 +0200 Subject: [PATCH 5/9] Update include/sentry.h Co-authored-by: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> --- include/sentry.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index cfd282c8b..a3a576a81 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2835,6 +2835,9 @@ SENTRY_EXPERIMENTAL_API void sentry_app_hang_heartbeat(void); * While paused, the watchdog does not capture app hangs. A subsequent heartbeat * from the watched thread automatically resumes detection. Heartbeats from * other threads continue to be ignored. + * + * This function is a no-op unless app-hang detection is enabled via + * `sentry_options_set_enable_app_hang_tracking`. */ SENTRY_EXPERIMENTAL_API void sentry_app_hang_pause(void); From 8c1046f97a7352ac85ca7514d44c46046a096d69 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 30 Jul 2026 12:25:05 +0200 Subject: [PATCH 6/9] moved pause to latch --- src/sentry_app_hang_latch.c | 9 +++++++++ src/sentry_app_hang_monitor.c | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sentry_app_hang_latch.c b/src/sentry_app_hang_latch.c index c9dc113ab..d9b23940f 100644 --- a/src/sentry_app_hang_latch.c +++ b/src/sentry_app_hang_latch.c @@ -141,3 +141,12 @@ sentry_app_hang_heartbeat(void) sentry__atomic_store(&g_app_hang_paused, 0); } } + +void +sentry_app_hang_pause(void) +{ + if (!sentry__app_hang_is_active()) { + return; + } + sentry__app_hang_set_paused(true); +} diff --git a/src/sentry_app_hang_monitor.c b/src/sentry_app_hang_monitor.c index 8add2b944..605402854 100644 --- a/src/sentry_app_hang_monitor.c +++ b/src/sentry_app_hang_monitor.c @@ -187,12 +187,3 @@ sentry__app_hang_monitor_stop(void) } #endif // SENTRY_HAS_THREAD_STACKWALK - -void -sentry_app_hang_pause(void) -{ - if (!sentry__app_hang_is_active()) { - return; - } - sentry__app_hang_set_paused(true); -} From 3a3fa25144f07736856cf632ba3141c0d8614200 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 30 Jul 2026 12:28:35 +0200 Subject: [PATCH 7/9] removed timestamp reset --- src/sentry_app_hang_latch.c | 5 ----- tests/unit/test_app_hang.c | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/sentry_app_hang_latch.c b/src/sentry_app_hang_latch.c index d9b23940f..75d8dd8b6 100644 --- a/src/sentry_app_hang_latch.c +++ b/src/sentry_app_hang_latch.c @@ -71,11 +71,6 @@ sentry__app_hang_is_active(void) void sentry__app_hang_set_paused(bool paused) { - if (paused) { - // Clear the old timestamp before publishing the paused state, so a - // concurrent heartbeat cannot leave detection armed with stale data. - sentry__atomic_store_u64(&g_last_heartbeat_ms, 0); - } sentry__atomic_store(&g_app_hang_paused, paused ? 1 : 0); } diff --git a/tests/unit/test_app_hang.c b/tests/unit/test_app_hang.c index 3d417867d..3dce0963f 100644 --- a/tests/unit/test_app_hang.c +++ b/tests/unit/test_app_hang.c @@ -62,7 +62,7 @@ SENTRY_TEST(app_hang_pause_resumes_on_heartbeat) l = sentry__app_hang_current_latch(); TEST_CHECK(sentry__app_hang_is_paused()); TEST_CHECK(l.target_tid == target); - TEST_CHECK(l.last_heartbeat_ms == 0); + TEST_CHECK(l.last_heartbeat_ms == first_heartbeat); sleep_ms(1); sentry_app_hang_heartbeat(); From 6464073e4605f3779a8e4ce87cebc65ed0a0c772 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 30 Jul 2026 13:03:26 +0200 Subject: [PATCH 8/9] added more complete tests --- src/sentry_app_hang_monitor.h | 4 ++++ tests/unit/test_app_hang.c | 37 +++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 42 insertions(+) diff --git a/src/sentry_app_hang_monitor.h b/src/sentry_app_hang_monitor.h index c97d9fed9..1c8b05059 100644 --- a/src/sentry_app_hang_monitor.h +++ b/src/sentry_app_hang_monitor.h @@ -9,7 +9,11 @@ struct sentry_options_s; // Interval at which the watchdog samples the heartbeat. +#if defined(SENTRY_UNITTEST) +# define SENTRY_APP_HANG_POLL_MS 10 +#else #define SENTRY_APP_HANG_POLL_MS 500 +#endif // Smallest timeout the watchdog can resolve meaningfully. A genuine hang fires // somewhere in [timeout, timeout + POLL_MS) depending on the phase between the diff --git a/tests/unit/test_app_hang.c b/tests/unit/test_app_hang.c index 3dce0963f..176e8eee5 100644 --- a/tests/unit/test_app_hang.c +++ b/tests/unit/test_app_hang.c @@ -170,6 +170,43 @@ SENTRY_TEST(app_hang_monitor_fires) sentry__app_hang_monitor_set_stackwalk_fn(NULL); } +SENTRY_TEST(app_hang_pause_prevents_capture) +{ +#if !SENTRY_HAS_THREAD_STACKWALK + SKIP_TEST(); +#endif + g_app_hang_seen = 0; + g_app_hang_type[0] = '\0'; + sentry__app_hang_latch_reset(); + sentry__app_hang_monitor_set_stackwalk_fn(fake_stackwalk); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_before_send(options, capture_before_send, NULL); + sentry_options_set_enable_app_hang_tracking(options, 1); + sentry_options_set_app_hang_timeout(options, 50); + sentry_init(options); + + sentry_app_hang_heartbeat(); + sentry_app_hang_pause(); + + // Wait beyond the timeout and several poll cycles while the worker is + // paused. + sleep_ms(100); + TEST_CHECK(sentry__atomic_fetch(&g_app_hang_seen) == 0); + + sentry_app_hang_heartbeat(); + for (int i = 0; i < 300 && !sentry__atomic_fetch(&g_app_hang_seen); i++) { + sleep_ms(10); + } + + TEST_CHECK(sentry__atomic_fetch(&g_app_hang_seen) == 1); + TEST_CHECK_STRING_EQUAL(g_app_hang_type, "AppHang"); + + sentry_close(); + sentry__app_hang_monitor_set_stackwalk_fn(NULL); +} + SENTRY_TEST(app_hang_disarm_prevents_capture) { // Mirrors app_hang_monitor_fires, but disarms after latching. This is the diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9005748fd..b88e07453 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -3,6 +3,7 @@ XX(app_hang_end_to_end) XX(app_hang_latch) XX(app_hang_make_event) XX(app_hang_monitor_fires) +XX(app_hang_pause_prevents_capture) XX(app_hang_pause_resumes_on_heartbeat) XX(app_hang_should_capture) XX(assert_sdk_name) From b6d804ff0421d18b9b4933c49cdc11a19b8a593d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 30 Jul 2026 13:05:11 +0200 Subject: [PATCH 9/9] style --- include/sentry.h | 2 +- src/sentry_app_hang_monitor.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index a3a576a81..f86190147 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2835,7 +2835,7 @@ SENTRY_EXPERIMENTAL_API void sentry_app_hang_heartbeat(void); * While paused, the watchdog does not capture app hangs. A subsequent heartbeat * from the watched thread automatically resumes detection. Heartbeats from * other threads continue to be ignored. - * + * * This function is a no-op unless app-hang detection is enabled via * `sentry_options_set_enable_app_hang_tracking`. */ diff --git a/src/sentry_app_hang_monitor.h b/src/sentry_app_hang_monitor.h index 1c8b05059..9ecad56f8 100644 --- a/src/sentry_app_hang_monitor.h +++ b/src/sentry_app_hang_monitor.h @@ -12,7 +12,7 @@ struct sentry_options_s; #if defined(SENTRY_UNITTEST) # define SENTRY_APP_HANG_POLL_MS 10 #else -#define SENTRY_APP_HANG_POLL_MS 500 +# define SENTRY_APP_HANG_POLL_MS 500 #endif // Smallest timeout the watchdog can resolve meaningfully. A genuine hang fires