Skip to content
Draft
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
42 changes: 23 additions & 19 deletions src/sentry_app_hang_monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ static bool g_running = false;
static sentry_threadid_t g_thread;
static sentry_mutex_t g_wait_mutex = SENTRY__MUTEX_INIT;
static sentry_cond_t g_wait_cond;
static sentry_clock_waiter_t g_waiter;
static uint64_t g_timeout_ms = 0;

static size_t
Expand Down Expand Up @@ -92,8 +93,12 @@ worker(void *arg)
uint64_t last_fired_hb = 0;
while (sentry__app_hang_is_active()) {
sentry__mutex_lock(&g_wait_mutex);
sentry__cond_wait_timeout(
&g_wait_cond, &g_wait_mutex, SENTRY_APP_HANG_POLL_MS);
if (!sentry__app_hang_is_active()) {
sentry__mutex_unlock(&g_wait_mutex);
break;
Comment on lines +96 to +98

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Covers race between outer while condition and acquiring g_wait_mutex.

}
sentry__clock_waiter_wait_locked(
&g_waiter, SENTRY_APP_HANG_POLL_MS);
sentry__mutex_unlock(&g_wait_mutex);

if (!sentry__app_hang_is_active()) {
Expand All @@ -105,22 +110,18 @@ worker(void *arg)

const sentry_app_hang_latch_t latch = sentry__app_hang_current_latch();
uint64_t now = sentry__monotonic_time();
if (sentry__app_hang_should_capture(
latch.last_heartbeat_ms, now, g_timeout_ms, last_fired_hb)) {
// Bail if disarmed. Keeps duplicate reporting window minimal
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.
if (app_hang_capture(
now - latch.last_heartbeat_ms, latch.target_tid)) {
last_fired_hb = latch.last_heartbeat_ms;
}
if (!sentry__app_hang_should_capture(latch.last_heartbeat_ms, now,
g_timeout_ms, last_fired_hb)) {
continue;
}

// Re-check state immediately before stackwalking to avoid reporting
// while crash handling or an explicit pause disarmed monitoring.
if (!sentry__app_hang_is_active() || sentry__app_hang_is_paused()) {
continue;
}
if (app_hang_capture(now - latch.last_heartbeat_ms, latch.target_tid)) {
last_fired_hb = latch.last_heartbeat_ms;
}
}
return 0;
Expand All @@ -135,11 +136,13 @@ sentry__app_hang_monitor_start(const sentry_options_t *options)

g_timeout_ms = options->app_hang_timeout;
sentry__cond_init(&g_wait_cond);
sentry__clock_waiter_init(&g_waiter, &g_wait_cond, &g_wait_mutex);
// Arm before spawning: the worker uses is_active() as its run condition, so
// it must already be true when the new thread first evaluates the loop.
sentry__app_hang_set_active(true);
if (sentry__thread_spawn(&g_thread, worker, NULL) != 0) {
sentry__app_hang_set_active(false);
sentry__clock_waiter_deinit(&g_waiter);
SENTRY_WARN("app-hang: failed to spawn watchdog thread");
return 1;
}
Expand All @@ -157,10 +160,11 @@ sentry__app_hang_monitor_stop(void)
}
sentry__app_hang_set_active(false);
sentry__mutex_lock(&g_wait_mutex);
sentry__cond_wake(&g_wait_cond);
sentry__clock_waiter_wake_locked(&g_waiter);
sentry__mutex_unlock(&g_wait_mutex);
sentry__thread_join(g_thread);
sentry__thread_free(&g_thread);
sentry__clock_waiter_deinit(&g_waiter);
sentry__app_hang_latch_reset();
g_running = false;
// g_timeout_ms are intentionally NOT cleared here: the worker
Expand Down
6 changes: 1 addition & 5 deletions src/sentry_app_hang_monitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@
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
#define SENTRY_APP_HANG_POLL_MS 500

// Smallest timeout the watchdog can resolve meaningfully. A genuine hang fires
// somewhere in [timeout, timeout + POLL_MS) depending on the phase between the
Expand Down
42 changes: 42 additions & 0 deletions src/sentry_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,48 @@
#include <string.h>
#include <time.h>

void
sentry__clock_waiter_init(sentry_clock_waiter_t *waiter, sentry_cond_t *cond,
sentry_mutex_t *mutex)
{
waiter->mutex = mutex;
waiter->cond = cond;
#ifdef SENTRY_UNITTEST
sentry__test_clock_waiter_init(waiter);
#endif
}

void
sentry__clock_waiter_deinit(sentry_clock_waiter_t *waiter)
{
#ifdef SENTRY_UNITTEST
sentry__test_clock_waiter_deinit(waiter);
#else
(void)waiter;
#endif
}

void
sentry__clock_waiter_wait_locked(
sentry_clock_waiter_t *waiter, uint64_t timeout_ms)
{
#ifdef SENTRY_UNITTEST
if (sentry__test_clock_waiter_wait_locked(waiter)) {
return;
}
#endif
sentry__cond_wait_timeout(waiter->cond, waiter->mutex, timeout_ms);
}

void
sentry__clock_waiter_wake_locked(sentry_clock_waiter_t *waiter)
{
#ifdef SENTRY_UNITTEST
sentry__test_clock_waiter_wake_locked(waiter);
#endif
sentry__cond_wake(waiter->cond);
}

#ifdef SENTRY_PLATFORM_DARWIN
# include <xlocale.h>
#elif defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID)
Expand Down
50 changes: 50 additions & 0 deletions src/sentry_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "sentry_boot.h"
#include "sentry_slice.h"
#include "sentry_sync.h"

#ifdef SENTRY_PLATFORM_DARWIN
# include <mach/clock.h>
Expand Down Expand Up @@ -164,12 +165,56 @@ char *sentry__dsn_get_minidump_url(
*/
char *sentry__base64_encode(const char *data, size_t len);

#ifdef SENTRY_UNITTEST
/**
* Overrides SDK time for deterministic unit tests. The clock is process-global,
* so callers must stop all SDK threads before resetting it.
*/
void sentry__test_clock_set(uint64_t monotonic_ms, uint64_t epoch_usec);
void sentry__test_clock_advance(uint64_t milliseconds);
void sentry__test_clock_reset(void);
bool sentry__test_clock_is_enabled(void);
uint64_t sentry__test_clock_monotonic_time(void);
uint64_t sentry__test_clock_usec_time(void);
#endif

typedef struct sentry_clock_waiter_s {
sentry_mutex_t *mutex;
sentry_cond_t *cond;
#ifdef SENTRY_UNITTEST
struct sentry_clock_waiter_s *next;
uint64_t requested_revision;
uint64_t seen_revision;
uint64_t wake_revision;
bool registered;
#endif
} sentry_clock_waiter_t;

#ifdef SENTRY_UNITTEST
void sentry__test_clock_waiter_init(sentry_clock_waiter_t *waiter);
void sentry__test_clock_waiter_deinit(sentry_clock_waiter_t *waiter);
bool sentry__test_clock_waiter_wait_locked(sentry_clock_waiter_t *waiter);
void sentry__test_clock_waiter_wake_locked(sentry_clock_waiter_t *waiter);
#endif

void sentry__clock_waiter_init(sentry_clock_waiter_t *waiter,
sentry_cond_t *cond, sentry_mutex_t *mutex);
void sentry__clock_waiter_deinit(sentry_clock_waiter_t *waiter);
void sentry__clock_waiter_wait_locked(
sentry_clock_waiter_t *waiter, uint64_t timeout_ms);
void sentry__clock_waiter_wake_locked(sentry_clock_waiter_t *waiter);

/**
* Returns the number of microseconds since the unix epoch.
*/
static inline uint64_t
sentry__usec_time(void)
{
#ifdef SENTRY_UNITTEST
if (sentry__test_clock_is_enabled()) {
return sentry__test_clock_usec_time();
}
#endif
#ifdef SENTRY_PLATFORM_WINDOWS
// Contains a 64-bit value representing the number of 100-nanosecond
// intervals since January 1, 1601 (UTC).
Expand Down Expand Up @@ -198,6 +243,11 @@ sentry__usec_time(void)
static inline uint64_t
sentry__monotonic_time(void)
{
#ifdef SENTRY_UNITTEST
if (sentry__test_clock_is_enabled()) {
return sentry__test_clock_monotonic_time();
}
#endif
#ifdef SENTRY_PLATFORM_WINDOWS
static LARGE_INTEGER qpc_frequency = { { 0, 0 } };

Expand Down
Loading
Loading