Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Native/Windows: Resolve correct symbol names for crashes in multi-module apps ([#1811](https://github.com/getsentry/sentry-native/pull/1811))

**Features**:

- Added an in-process app-hang detection. When enabled via `sentry_options_set_enable_app_hang_tracking`, a background thread monitors the application and captures an app-hang event if no heartbeat is received within `app_hang_timeout_ms` (default `5000` ms). Call `sentry_app_hang_heartbeat()` regularly from the thread you want monitored. ([#1806](https://github.com/getsentry/sentry-native/pull/1806))

## 0.15.1

**Fixes**:
Expand All @@ -15,7 +19,7 @@

**Internal**:

- Refactor envelope writers to better support failure tracking on each layer and not push check responsibility to client code. ([#1807](https://github.com/getsentry/sentry-native/pull/1807))
- Refactor envelope writers to better support failure tracking on each layer and not push check responsibility to client code. ([#1807](https://github.com/getsentry/sentry-native/pull/1807))

## 0.15.0

Expand Down
27 changes: 27 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,11 @@ main(int argc, char **argv)
sentry_options_set_enable_large_attachments(options, 1);
}

if (has_arg(argc, argv, "app-hang")) {
sentry_options_set_enable_app_hang_tracking(options, 1);
sentry_options_set_app_hang_timeout_ms(options, 1000);
}

if (has_arg(argc, argv, "stdout")) {
sentry_options_set_transport(
options, sentry_transport_new(print_envelope));
Expand Down Expand Up @@ -1144,6 +1149,28 @@ main(int argc, char **argv)
sleep_s(10);
}

if (has_arg(argc, argv, "app-hang")) {
printf("app-hang: start\n");
fflush(stdout);

// A couple of heartbeats to latch this (main) thread as the monitored
// thread and keep it fresh.
for (int i = 0; i < 3; i++) {
sentry_app_hang_heartbeat();
sleep_ms(100);
}

printf("app-hang: doing some heavy work now (going to sleep)\n");
fflush(stdout);

// Block the monitored thread past the configured timeout so the
// watchdog samples this hung thread and captures an AppHang event.
sleep_s(3);

printf("app-hang: finishing\n");
fflush(stdout);
}

if (has_arg(argc, argv, "test-logger-before-crash")) {
// Output marker directly using printf for test parsing
printf("pre-crash-log-message\n");
Expand Down
45 changes: 45 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -2651,6 +2651,51 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_metrics(
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_metrics(
const sentry_options_t *opts);

/**
* Enables or disables in-process app-hang detection. When enabled, a
* background watchdog thread monitors heartbeats from the watched thread. If
* no heartbeat is received within the configured timeout, an app-hang event is
* captured and sent to Sentry.
*
* Disabled by default. Must be combined with regular calls to
* `sentry_app_hang_heartbeat()` from the thread you want monitored.
*/
SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_app_hang_tracking(
sentry_options_t *opts, int enable);
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_app_hang_tracking(
const sentry_options_t *opts);

/**
* Sets the app-hang detection timeout in milliseconds. Defaults to 5000 ms.
* If `enable_app_hang_tracking` is true and no heartbeat is received within
* this window, an app-hang event is captured.
*
* Setting this to 0 while `enable_app_hang_tracking` is true is a
* configuration error: the watchdog will log a warning and skip detection.
*/
Comment thread
cursor[bot] marked this conversation as resolved.
SENTRY_EXPERIMENTAL_API void sentry_options_set_app_hang_timeout_ms(
sentry_options_t *opts, uint64_t millis);
SENTRY_EXPERIMENTAL_API uint64_t sentry_options_get_app_hang_timeout_ms(
const sentry_options_t *opts);

/**
* Records a heartbeat from the calling thread.
*
* The first call latches the calling thread as the monitored thread.
* Call this regularly from the thread you want watched for hangs. If the
* watchdog does not receive a heartbeat within the configured timeout, it
* captures an app-hang event.
*
* Only a single thread is monitored: the watchdog tracks exactly the one
* thread latched by the first heartbeat. Calls from any other thread are
* ignored. To watch the thread most representative of responsiveness (e.g.
* a UI or main loop), call this from that thread first.
*
* 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_heartbeat(void);
Comment thread
bitsandfoxes marked this conversation as resolved.

/**
* Type of the `before_send_metric` callback.
*
Expand Down
24 changes: 24 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
sentry_target_sources_cwd(sentry
sentry_alloc.c
sentry_alloc.h
sentry_app_hang_latch.c
sentry_app_hang_latch.h
sentry_app_hang_monitor.c
sentry_app_hang_monitor.h
sentry_attachment.c
sentry_attachment.h
sentry_backend.c
Expand Down Expand Up @@ -54,6 +58,7 @@ sentry_target_sources_cwd(sentry
sentry_symbolizer.h
sentry_sync.c
sentry_sync.h
sentry_thread_stackwalk.h
sentry_transport.c
sentry_transport.h
sentry_utils.c
Expand Down Expand Up @@ -256,6 +261,25 @@ if(SENTRY_WITH_LIBUNWIND_MAC)
)
endif()

# platform thread stackwalker (suspend a thread and capture its backtrace)
if(APPLE)
sentry_target_sources_cwd(sentry
sentry_thread_stackwalk_mach.c
)
endif()

if(WIN32)
sentry_target_sources_cwd(sentry
sentry_thread_stackwalk_windows.c
)
endif()

if(LINUX OR ANDROID)
sentry_target_sources_cwd(sentry
sentry_thread_stackwalk_posix.c
)
endif()

if(SENTRY_WITH_LIBUNWINDSTACK)
target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBUNWINDSTACK)
sentry_target_sources_cwd(sentry
Expand Down
115 changes: 115 additions & 0 deletions src/sentry_app_hang_latch.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// In-process app-hang detection, shared state and helpers. The latch, heartbeat
// API, and capture predicate are the app-thread hot path: app threads write the
// latch via the heartbeat (timestamped with sentry__monotonic_time), the
// watchdog worker in sentry_app_hang_monitor.c reads it.
#include "sentry_app_hang_latch.h"
#include "sentry_sync.h"
#include "sentry_utils.h"

#include <stdint.h>

#if defined(SENTRY_PLATFORM_WINDOWS)
# include <windows.h>
#elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
# include <sys/syscall.h>
# include <unistd.h>
#else // SENTRY_PLATFORM_MACOS and other POSIX
# include <pthread.h>
#endif

bool
sentry__app_hang_should_capture(
uint64_t hb, uint64_t now, uint64_t timeout_ms, uint64_t last_fired_hb)
{
if (hb == 0 || timeout_ms == 0) {
return false;
}
if (now < hb || (now - hb) < timeout_ms) {
return false;
}
if (hb == last_fired_hb) {
return false; // already fired for this freeze
}
return true;
}

// The latch is touched by app threads (writers, via the heartbeat) and the
// single watchdog worker (reader). An explicit mutex would put a lock on the
// heartbeat hot path, not ideal.
// The two fields are accessed with 64-bit atomics:
//
// - last_heartbeat_ms: written on every heartbeat, read by the worker.
// Needs 64-bit-atomic access so a 32-bit platform can't observe a torn
// half-updated timestamp.
// - target_tid: write-once (0 -> first heartbeating tid). The worker only
// uses it as a stackwalker argument so a relaxed read is sufficient.
//
// Both fields use the sentry__atomic_*_u64 helpers, which provide full 64-bit
// atomic (tear-free) access on every platform. Where the target has native
// 64-bit atomics (AArch64, x86-64, ...) this is lock-free; on ARMv7 the
// compiler lowers it to a libatomic call backed by a lock pool. That is fine
// here: the access is off any signal handler (both writer and worker run in
// normal thread context) and the heartbeat cadence dwarfs the few-ns lock, so
// the lock only delivers the tear-free guarantee we already need.
static uint64_t g_target_tid = 0;
static uint64_t g_last_heartbeat_ms = 0;
static volatile long g_app_hang_active = 0;

void
sentry__app_hang_set_active(bool active)
{
sentry__atomic_store(&g_app_hang_active, active ? 1 : 0);
}

uint64_t
sentry__app_hang_current_tid(void)
{
#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
return (uint64_t)syscall(SYS_gettid);
#elif defined(SENTRY_PLATFORM_MACOS)
uint64_t tid = 0;
pthread_threadid_np(pthread_self(), &tid);
return tid;
#elif defined(SENTRY_PLATFORM_WINDOWS)
return (uint64_t)GetCurrentThreadId();
#else
return 0;
#endif
}

sentry_app_hang_latch_t
sentry__app_hang_current_latch(void)
{
sentry_app_hang_latch_t latch;
latch.target_tid = sentry__atomic_fetch_u64(&g_target_tid);
latch.last_heartbeat_ms = sentry__atomic_fetch_u64(&g_last_heartbeat_ms);
return latch;
}

void
sentry__app_hang_latch_reset(void)
{
sentry__atomic_store_u64(&g_target_tid, 0);
sentry__atomic_store_u64(&g_last_heartbeat_ms, 0);
}

void
sentry_app_hang_heartbeat(void)
{
if (!sentry__atomic_fetch(&g_app_hang_active)) {
return;
}
uint64_t tid = sentry__app_hang_current_tid();

uint64_t target = sentry__atomic_fetch_u64(&g_target_tid);
if (target == 0) {
// Latch the first heartbeating thread.
sentry__atomic_store_u64(&g_target_tid, tid);
target = tid;
}
Comment thread
bitsandfoxes marked this conversation as resolved.
if (target == tid) {
// ignore heartbeats from other threads
sentry__atomic_store_u64(
&g_last_heartbeat_ms, sentry__monotonic_time());
}
Comment thread
sentry[bot] marked this conversation as resolved.
}
29 changes: 29 additions & 0 deletions src/sentry_app_hang_latch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifndef SENTRY_APP_HANG_LATCH_H_INCLUDED
#define SENTRY_APP_HANG_LATCH_H_INCLUDED

#include "sentry_boot.h"
#include "sentry_value.h"

#define SENTRY_APP_HANG_MAX_FRAMES 128

bool sentry__app_hang_should_capture(
uint64_t hb, uint64_t now, uint64_t timeout_ms, uint64_t last_fired_hb);

typedef struct {
uint64_t target_tid;
uint64_t last_heartbeat_ms;
} sentry_app_hang_latch_t;

uint64_t sentry__app_hang_current_tid(void);
sentry_app_hang_latch_t sentry__app_hang_current_latch(void);
void sentry__app_hang_latch_reset(void);

// Enables/disables the heartbeat fast-path. The watchdog monitor sets this on
// start and clears it on stop, so sentry_app_hang_heartbeat() is a cheap no-op
// when detection is not running.
void sentry__app_hang_set_active(bool active);

sentry_value_t sentry__app_hang_make_event(
void **ips, size_t frame_count, uint64_t freeze_ms);

#endif
Loading
Loading