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
44 changes: 44 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,24 @@ run_threads(thread_func_t func)
}
#endif

#if defined(SENTRY_PLATFORM_WINDOWS)
static unsigned __stdcall
app_hang_demo_thread(void *arg)
{
(void)arg;
/* Latch this thread as the target once, then heartbeat for 500 ms so the
* daemon sees a healthy baseline before the freeze. */
sentry_app_hang_set_target_thread();
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 @@ -879,6 +897,13 @@ main(int argc, char **argv)
options, SENTRY_CRASH_UPLOAD_MODE_ASYNC);
}

#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 @@ -890,6 +915,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
59 changes: 59 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,65 @@ 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);

/**
* Designate the calling thread as the one monitored by the app-hang detector.
*
* Call this once, from the thread you want monitored (typically the main /
* game thread), before the first heartbeat. The latch is sticky for the
* lifetime of the SDK session: subsequent calls from any other thread are
* dropped. Calling again from the same thread is a harmless no-op.
*
* Until this is called, `sentry_app_hang_heartbeat()` is a no-op — there is
* no implicit "first caller wins" latch, so a stray heartbeat from a worker
* thread during startup cannot accidentally claim the role and silently
* disable monitoring of the real main thread.
*
* 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_set_target_thread(void);

/**
* Refresh the heartbeat for the monitored thread.
*
* Call this from the thread previously designated via
* `sentry_app_hang_set_target_thread()`. Calls from any other thread, or
* before a target has been set, 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 @@ -325,6 +325,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