Skip to content

test(benchmark): crash handler stack usage - #2029

Open
jpnurmi wants to merge 1 commit into
jpnurmi/test/benchmark-libsizefrom
jpnurmi/test/benchmark-stack-usage
Open

test(benchmark): crash handler stack usage#2029
jpnurmi wants to merge 1 commit into
jpnurmi/test/benchmark-libsizefrom
jpnurmi/test/benchmark-stack-usage

Conversation

@jpnurmi

@jpnurmi jpnurmi commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Measure how much stack each crash backend uses while processing a fatal error.

Boring details for the curious

When a thread first enters crash handling, the fixture fills a 12 KiB local array with a recognizable byte pattern. The helper then returns, leaving the pattern in stack space available for reuse. As the real crash handler runs, its call frames overwrite that pattern.

Before the thread leaves crash handling, the fixture scans the remaining pattern. The deepest overwritten position is reported as [STACK] N bytes. Nested entries on the same thread form one measurement, while separate handler intervals or threads may produce multiple values. The benchmark records the largest value because these are independent stack peaks, not additive memory usage.

Crash callbacks may run on alternate signal stacks as small as 16 KiB. Painting 12 KiB leaves the remaining 4 KiB for the signal frame and callback machinery while paint_stack is active. Painting the full 16 KiB would risk overflowing the stack before measurement begins.

Add integration callbacks around each backend's crash-handling work so the fixture can apply the same measurement consistently. These callbacks must remain async-signal-safe because some backends invoke them from a signal handler.

Use a dummy custom HTTP client so the measured path includes the HTTP transport's crash-time queue drain without depending on network I/O.

Share the SDK target-property helper used by source-compiling test targets instead of duplicating it in each CMake file.

The stack usage measurements are published in benchmark charts and monitored over time:

Screenshot From 2026-08-27 17-23-15

To inspect stack usage locally:

$ cmake -S . -B build -DSENTRY_BACKEND=inproc -DSENTRY_BUILD_TESTS=ON -DSENTRY_BUILD_BENCHMARKS=ON
$ cmake --build build --target sentry_stack_usage
$ ./build/tests/fixtures/stack_usage/sentry_stack_usage
[...]
[STACK] 0 bytes
[...]
[STACK] 7512 bytes
[STACK] 336 bytes
Segmentation fault (core dumped)

The number and values of the measurements depend on the backend and platform. The final crash is intentional.

Separately, add make stack-frames for manual inspection of the functions compiled into libsentry. It builds the selected backend with -fstack-usage and prints the 20 largest compiler-reported function frames. This does not measure a call chain or runtime high-water mark and is not published as a benchmark result.

$ make stack-frames SENTRY_BACKEND=inproc N=50
4192    src/unwinder/sentry_unwinder_libunwind.c:66:5:find_mem_range_from_fd    static
[...]

Close: #997

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b0d7092. Configure here.

Comment thread src/backends/sentry_backend_inproc.c
Comment thread src/backends/sentry_backend_inproc.c
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 37.20930% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.28%. Comparing base (989606e) to head (8a1133b).

Additional details and impacted files
@@                        Coverage Diff                         @@
##           jpnurmi/test/benchmark-libsize    #2029      +/-   ##
==================================================================
+ Coverage                           74.27%   74.28%   +0.01%     
==================================================================
  Files                                 104      104              
  Lines                               26627    26670      +43     
  Branches                             4844     4850       +6     
==================================================================
+ Hits                                19777    19813      +36     
- Misses                               5502     5506       +4     
- Partials                             1348     1351       +3     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Crash handlers often run with little stack headroom, including alternate
signal stacks as small as 16 KiB. Measure the real crash path so stack growth
can be caught before it becomes an overflow.

Add async-signal-safe integration callbacks around crash-handler work in each
backend. A dedicated fixture uses those boundaries to paint 12 KiB of stack
with a recognizable pattern and then returns, making the painted space
available to the handler. At handler exit it finds the deepest overwritten
byte and reports that interval's high-water mark. The benchmark publishes the
largest interval for each backend because separate intervals are independent
peaks rather than values that should be added together.

The 12 KiB window leaves 4 KiB of a 16 KiB alternate signal stack for the
signal frame and measurement callback while the pattern is installed. Once
the painting function returns, the full painted window is available to the
real handler.

Use a dummy HTTP client so the measured path includes the HTTP transport's
crash-time queue drain without depending on network I/O. Skip Crashpad on
macOS because it has no first-chance handler there.

The stack usage measurements are published in benchmark charts and monitored
over time:

<img
  width="2070"
  height="1010"
  alt="Screenshot From 2026-08-27 17-23-15"
  src="https://github.com/user-attachments/assets/150e0d0c-a23c-4368-aaed-5903c925070f"
/>

To run the benchmark fixture locally:

    $ cmake -S . -B build \
        -DSENTRY_BUILD_TESTS=ON \
        -DSENTRY_BUILD_BENCHMARKS=ON \
        -DSENTRY_BACKEND=inproc
    $ cmake --build build --target sentry_stack_usage
    $ ./build/tests/fixtures/stack_usage/sentry_stack_usage
    [...]
    [STACK] 0 bytes
    [...]
    [STACK] 7560 bytes
    [STACK] 336 bytes
    Segmentation fault (core dumped)

The final crash is intentional. The number of handler intervals and their
values depend on the backend and platform.

Separately, add `make stack-frames` for manual inspection of the functions
compiled into `libsentry`. It builds the selected backend with
`-fstack-usage` and prints the 20 largest compiler-reported function frames.
This does not measure a call chain or runtime high-water mark and is not
published as a benchmark result.

    $ make stack-frames SENTRY_BACKEND=inproc
    4192    src/unwinder/sentry_unwinder_libunwind.c:66:5:find_mem_range_from_fd    static
    [...]

Use `N` to change the default limit:

    $ make stack-frames SENTRY_BACKEND=native N=50

Fixes: #997
@jpnurmi
jpnurmi force-pushed the jpnurmi/test/benchmark-stack-usage branch from fe71085 to 8a1133b Compare August 28, 2026 14:29
@jpnurmi jpnurmi changed the title test: benchmark crash handler stack usage test(benchmark): crash handler stack usage Aug 28, 2026
@jpnurmi
jpnurmi changed the base branch from master to jpnurmi/test/benchmark-libsize August 28, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add static+dynamic stack usage measures and alerts in CI

1 participant