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
43 changes: 43 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#ifdef SENTRY_PLATFORM_WINDOWS
# include <malloc.h>
# include <process.h>
# include <synchapi.h>
# define sleep_s(SECONDS) Sleep((SECONDS) * 1000)
#else
Expand Down Expand Up @@ -533,6 +534,22 @@ run_threads(thread_func_t func)
}
#endif

#if defined(SENTRY_PLATFORM_WINDOWS)
static unsigned __stdcall
app_hang_demo_thread(void *arg)
{
(void)arg;
/* Heartbeat for 500 ms to latch this thread as the target. */
for (int i = 0; i < 10; i++) {
sentry_app_hang_heartbeat();
Sleep(50);
}
/* Freeze for 3x the configured timeout (3000 ms). */
Sleep(3000);
return 0;
}
#endif

int
main(int argc, char **argv)
{
Expand Down Expand Up @@ -784,6 +801,13 @@ main(int argc, char **argv)
}
}

#if defined(SENTRY_PLATFORM_WINDOWS)
if (has_arg(argc, argv, "app-hang")) {
sentry_options_set_app_hang_enabled(options, 1);
sentry_options_set_app_hang_timeout_ms(options, 1000);
}
#endif

// E2E test mode: generate unique test ID for event correlation
char e2e_test_id[37] = { 0 };
if (has_arg(argc, argv, "e2e-test")) {
Expand All @@ -795,6 +819,25 @@ main(int argc, char **argv)
return EXIT_FAILURE;
}

#if defined(SENTRY_PLATFORM_WINDOWS)
/* app-hang: spawn the demo thread BEFORE any other post-init work so it
* begins heartbeating immediately. The thread freezes for 3x the timeout,
* giving the daemon time to detect the hang and ship the envelope. We wait
* for it here so main does not exit before the transport has flushed.
* NOTE: this mode is intentionally exclusive – do not combine with crash/
* abort/etc. since those would terminate the process first. */
if (has_arg(argc, argv, "app-hang")) {
HANDLE t = (HANDLE)_beginthreadex(
NULL, 0, app_hang_demo_thread, NULL, 0, NULL);
if (t) {
WaitForSingleObject(t, INFINITE);
CloseHandle(t);
}
sentry_close();
return EXIT_SUCCESS;
}
#endif

if (has_arg(argc, argv, "user-consent-revoke")) {
sentry_user_consent_revoke();
}
Expand Down
42 changes: 42 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1678,6 +1678,48 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_attach_session_replay(
SENTRY_EXPERIMENTAL_API void sentry_options_set_session_replay_duration(
sentry_options_t *opts, uint32_t duration_ms);

/**
* Enable app-hang detection in the native crash backend.
*
* When enabled, the out-of-process daemon monitors a designated thread in the
* host via a shared-memory heartbeat. If the heartbeat goes stale for longer
* than the configured timeout, the daemon walks the thread's stack remotely and
* emits an `ApplicationNotResponding` event. The host process keeps running.
*
* Off by default. This setting only has an effect when using the `native`
* backend. In this initial release the feature is Windows-only; the call is a
* silent no-op on other platforms.
*/
SENTRY_EXPERIMENTAL_API void sentry_options_set_app_hang_enabled(
sentry_options_t *opts, int enabled);

/**
* Sets the heartbeat-staleness threshold (in milliseconds) used by the
* app-hang detector. Default 5000 ms.
*
* Read by the daemon once at startup; changes after `sentry_init` have no
* effect.
*/
SENTRY_EXPERIMENTAL_API void sentry_options_set_app_hang_timeout_ms(
sentry_options_t *opts, uint64_t timeout_ms);

/**
* Signal that the calling thread is alive.
*
* Call this from the thread you want monitored (typically the main / game
* thread). The first call latches the calling thread's id as the target;
* subsequent calls from the same thread refresh the heartbeat timestamp. Calls
* from any other thread are dropped — so a stray heartbeat from a worker
* thread cannot mask a frozen main thread.
*
* Cost: approximately one system call plus a relaxed 64-bit store. Safe to
* call from a per-frame hook in a game engine.
*
* No-op if app-hang detection is not enabled in options, or if the native
* backend is not active, or on non-Windows platforms.
*/
SENTRY_EXPERIMENTAL_API void sentry_app_hang_heartbeat(void);

/**
* Sets the path to the crashpad handler if the crashpad backend is used.
*
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
sentry_target_sources_cwd(sentry
sentry_alloc.c
sentry_alloc.h
sentry_app_hang.c
sentry_app_hang.h
sentry_attachment.c
sentry_attachment.h
sentry_backend.c
Expand Down
16 changes: 16 additions & 0 deletions src/backends/native/sentry_crash_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,22 @@ typedef struct {
uint32_t module_count;
sentry_module_info_t modules[SENTRY_CRASH_MAX_MODULES];

/* App-hang detection (Windows-only, native backend only).
*
* Sync model:
* - app_hang_enabled, app_hang_timeout_ms: written by host before daemon
* is signalled ready; read by daemon at startup. No further mutation.
* - app_hang_target_tid: latched once by host on first heartbeat (release
* store via InterlockedCompareExchange64). Daemon reads, never writes.
* - app_hang_last_heartbeat_ms: written on every heartbeat with a relaxed
* 64-bit store. Daemon reads with a relaxed load. Torn reads are not a
* correctness issue — the daemon compares against its remembered value
* from the previous tick. */
bool app_hang_enabled;
uint64_t app_hang_timeout_ms;
volatile uint64_t app_hang_target_tid;
volatile uint64_t app_hang_last_heartbeat_ms;

} sentry_crash_context_t;

// Shared memory size: calculated at compile-time based on actual struct size
Expand Down
Loading
Loading