Skip to content

fix(nativemem): skip building DWARF unwind tables when cstack mode doesn't need them - #755

Closed
rkennke wants to merge 1 commit into
mainfrom
fix-parseDwarfInfo-cstack-gate
Closed

fix(nativemem): skip building DWARF unwind tables when cstack mode doesn't need them#755
rkennke wants to merge 1 commit into
mainfrom
fix-parseDwarfInfo-cstack-gate

Conversation

@rkennke

@rkennke rkennke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • ElfParser::parseDwarfInfo() builds SFrame/DWARF unwind tables for every parsed native library unconditionally, even though they're read only by StackWalker::walkDwarf() under CSTACK_DWARF. Measured at 4.52 MiB / 22 allocations at 150,000 loaded classes, paid even when the resolved mode is CSTACK_VM (the common default whenever HotSpot VMStructs are available).
  • Adds a gate: skip building once we know for certain the mode isn't CSTACK_DWARF. Getting "certain" right requires a dedicated Profiler::_cstack_resolved flag, not just comparing cstackMode()_cstack's constructor default is CSTACK_NO, a real user-selectable mode, not a distinguishable "not yet decided" sentinel. Since libraries are parsed exactly once (Symbols::parseLibraries tracks _parsed_inodes), including the handful parsed at JVMTI Agent_OnLoad before Profiler::start() ever resolves cstack mode, a naive check would permanently and silently skip building the table for those libraries (libjvm.so among them) even in an actual CSTACK_DWARF session — degrading native-frame unwind quality with no visible error. _cstack_resolved is set once, right after start()'s resolution logic, before any sampling begins.

Honest note on measured impact — why this is closed rather than pursued further right now

The corrected fix is safe (verified cstack=dwarf still builds the full table, no regression). But re-measuring after fixing the above turned up something worth flagging: on the benchmark workload used to find this (classes mode, 150,000 loaded classes), the real savings are only ~0.07 MiB, not the ~4.52 MiB originally measured with the buggy gate. Almost the entire native-library set for that workload loads before Profiler::start() runs, where the gate must conservatively always build (the eventual mode isn't knowable yet). Only a handful of JDK-internal libraries that load lazily after start (libmanagement.so, libnet.so, libnio.so, etc.) actually benefit.

This fix should have more value for workloads that dlopen native libraries while actively profiling in a non-DWARF mode (JNI-heavy applications, dynamically-attached native agents) — but that scenario isn't covered by any workload we've measured, so the benefit there is currently unquantified, not just small. Given the added complexity (a new Profiler field threaded through a lifecycle-ordering invariant) isn't clearly justified by a measured benefit at this point, closing this without merging. Leaving it open here as reference in case someone has a workload where this matters and wants to pick it up — the fix itself is complete and tested.

Test plan

  • stackWalker_ut, libraries_ut, codeCache_ut (release+debug) all pass
  • Verified cstack=dwarf builds the full table (NM_NATIVE_SYMBOLS = 13.03 MiB, matching the ungated value exactly — no regression)
  • Verified cstack=fp correctly skips the ~0.07 MiB that's actually skippable for that workload

🤖 Generated with Claude Code

…esn't need them

ElfParser::parseDwarfInfo() previously built SFrame/DWARF unwind tables for
every parsed native library unconditionally, even though they are read only
by StackWalker::walkDwarf() under CSTACK_DWARF. Measured cost: 4.52 MiB
across 22 allocations at 150,000 loaded classes, even in the common case
where the default cstack mode resolves to CSTACK_VM (whenever HotSpot
VMStructs are available) rather than CSTACK_DWARF.

Gating this correctly needs more than comparing cstackMode() to CSTACK_DWARF:
Profiler::_cstack's constructor default is CSTACK_NO, a real user-selectable
mode, not a distinguishable "not yet resolved" sentinel. Since libraries are
parsed exactly once and never re-parsed (Symbols::parseLibraries tracks
_parsed_inodes) -- including the handful parsed at JVMTI Agent_OnLoad, before
the later VM::VMInit()-triggered Profiler::start() call resolves cstack mode
-- a naive comparison would permanently and silently skip building the table
for those libraries (libjvm.so among them) even in an actual CSTACK_DWARF
session, degrading native-frame unwind quality with no visible error.

Adds Profiler::_cstack_resolved, set once immediately after start()'s
resolution logic and before any sampling begins, to disambiguate "not yet
decided" from "decided, and it's CSTACK_NO". parseDwarfInfo() only skips
once cstackResolved() is true and the resolved mode isn't CSTACK_DWARF.

Verified: cstack=dwarf correctly builds the full table (NM_NATIVE_SYMBOLS
13.03 MiB, matching the pre-gate value exactly); stackWalker_ut, libraries_ut,
and codeCache_ut (release+debug) all pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rkennke
rkennke requested a review from a team as a code owner August 25, 2026 18:27
@rkennke

rkennke commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Closing without merging per the honest cost/benefit note above: the corrected fix is real and tested, but the measured benefit on the only workload we tested it against is ~0.07 MiB, not worth the added complexity right now. Left open as a reference/starting point for whoever has a workload where dynamically-loaded native libraries during active non-DWARF profiling actually matter.

@rkennke rkennke closed this Aug 25, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b436f83ec8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +727 to +729
if (Profiler::instance()->cstackResolved() &&
Profiler::instance()->cstackMode() != CSTACK_DWARF) {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rebuild skipped unwind tables before enabling DWARF

When an initial recording uses cstack=vm, fp, or no and a library is loaded during that session, this return creates its CodeCache without an unwind table, while Symbols::_parsed_inodes prevents that library from ever being parsed again. A later supported stop/start cycle with cstack=dwarf therefore walks frames from that library using the fallback descriptor instead of its SFrame/DWARF data, silently producing incorrect or truncated native stacks. Retain the unwind data or explicitly populate missing tables when transitioning to DWARF.

Useful? React with 👍 / 👎.

@dd-octo-sts

dd-octo-sts Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervmgx7h7
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Tue Aug 25 18:29:50 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs1
Logic error
Dereference of null pointer1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Logic errorDereference of null pointerprofiler.hfindLibraryByAddress54728

@dd-octo-sts

dd-octo-sts Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #32883840993 | Commit: b68990a | Duration: 15m 23s (longest job)

1 of 32 test jobs failed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - - -
8-ibm - - -
8-j9 - -
8-librca - -
8-orcl - - -
11 - - -
11-j9 - -
11-librca - -
17 - -
17-graal - -
17-j9 - -
17-librca - -
21 - -
21-graal - -
21-librca - -
25 - -
25-graal - -
25-librca - -

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Failed Tests

glibc-aarch64/debug / 8-j9

Job: View logs

No detailed failure information available. Check the job logs.

Summary: Total: 32 | Passed: 31 | Failed: 1


Updated: 2026-08-25 18:51:45 UTC

rkennke added a commit that referenced this pull request Aug 26, 2026
…fix, the bug it caught, and why it's closed

The gate was implemented and initially measured as a ~4.52 MiB win, but that
measurement was against a buggy version (Profiler::_cstack's constructor
default is CSTACK_NO, a real mode, not a distinguishable "unresolved"
sentinel -- a naive gate would have permanently and silently broken DWARF
unwinding for libjvm.so itself in an actual CSTACK_DWARF session). Fixed
properly with a dedicated _cstack_resolved flag, then re-measured: the real
benefit on our benchmark is ~0.07 MiB, not 4.52 MiB, because almost the
entire native-library set for that workload loads before Profiler::start()
resolves cstack, where the gate must conservatively always build.

Submitted as #755 and closed without merging -- the
fix is correct and tested, but not worth the added complexity given the
measured benefit, though it may help workloads that dlopen native libraries
while actively profiling (unmeasured here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant