fix(engine, java): reduce heap retention by releasing AST and detection state eagerly#417
fix(engine, java): reduce heap retention by releasing AST and detection state eagerly#417vdstech wants to merge 3 commits into
Conversation
b870b60 to
4169621
Compare
…n state eagerly Fixes cbomkit#371. Large Java codebases (e.g. 28 000-file projects) could retain hundreds of fully-resolved Java ASTs simultaneously because DetectionExecutive held a strong reference to the tree after analysis, and DetectionStore instances kept their full value/child collections alive until the end of the scan. Changes: - DetectionExecutive: null out tree in a finally block inside start() so the AST is eligible for GC immediately after rule analysis, even on exception. - DetectionExecutive: track deferred-hook registrations with a boolean flag; call releaseResources() eagerly when no deferred hooks are present, and expose releaseDeferredResources() / hasDeferredHooks() / isReleased() for the language layer to drive cleanup after all hooks have fired. - IStatusReporting: add default method onDeferredHookRegistration() as a backward-compatible lifecycle callback so DetectionExecutive can be notified when a deferred hook is registered without breaking existing implementations. - DetectionStore: add release() to recursively clear detectionValues, children, and actionValue; simplify ifPresentOrElse(..., () -> {}) calls to ifPresent. - JavaBaseDetectionRule: collect executives that registered deferred hooks and call releaseDeferredResources() on all of them in leaveFile(), ensuring state is freed after every file regardless of deferred activity. - JavaAggregator: add resetLanguageSupport() and call JavaScanMemoryLogger.reset() inside reset() to keep observability counters aligned with scan lifecycle. - JavaScanMemoryLogger: new lightweight utility that samples JVM heap usage and logs progress every N files, making memory regressions visible in scan logs. - OutputFileJob: retain JavaScanMemoryLogger.Snapshot logging for observability; no functional change to CBOM output path. Tests added: - DetectionExecutiveLifecycleTest (8 tests) - deferred hook state machine - DetectionStoreReleaseTest (7 tests) - recursive release correctness - JavaScanMemoryLoggerTest (9 tests) - counter, throttling, and reset behaviour Signed-off-by: Karunakar Mattaparthi <[email protected]> Co-authored-by: Cursor <[email protected]>
4169621 to
22ae4ac
Compare
ReviewThanks for tackling #371 — the core memory optimization is sound, well-tested (24 new tests match the claim), and SPI-safe (default method on What this PR gets right (worth calling out)
Should fix (non-blocking)
Suggestions
Performance validation requestThe PR description states "Peak heap dropped from >90 GB to normal scanner levels" on Elasticsearch (28k files) and Kafka (6k files), but no measurement artifacts are attached. Could you share a JFR recording comparing scan runs with and without this PR on the Elasticsearch codebase? Suggested capture: # Baseline: main branch
mvn org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \
-Dsonar.projectKey=elasticsearch \
-DargLine=\"-XX:StartFlightRecording=duration=0,filename=/tmp/baseline.jfr,settings=profile\"
# This PR's branch (same -Xmx, same profile, same -DargLine but filename=/tmp/pr417.jfr)Useful to see in the report:
Same |
Co-authored-by: Cursor <[email protected]>
Five review issues resolved: 1. JavaScanMemoryLogger - add field-level Javadoc on JAVA_FILES_PROCESSED and PEAK_USED_MB explaining JVM-global scope, multi-scan caveats, and the two-CAS reset race acceptable for metrics-only counters. Remove the unused SUCCESSFUL_FILE_STATE_RESETS counter and its Snapshot field; update test accordingly. 2. DetectionStore.release() - replace recursive DFS with an iterative collect-then-reverse post-order traversal backed by ArrayDeque so deep trees cannot cause StackOverflowError. Rename test methods that referred to isRecursive_ and add a 20000-node chain regression test. 3. DetectionExecutive.releaseResources() - clarify the defensive tree = null comment: start()'s finally already clears the reference; retained for safety against future independent calls. 4. IStatusReporting.onDeferredHookRegistration() - replace terse Javadoc with a full contract using @APinote, @implSpec, @implNote, and @SInCE so downstream language-module authors understand the deferred-hook lifecycle. Signed-off-by: Karunakar Mattaparthi <[email protected]> Co-authored-by: Cursor <[email protected]>
Performance Validation — JFR Memory AnalysisPR #417: Fix DetectionStore recursive release + memory lifecycleEnvironment: -Xmx8g, same host, same sonar-project.propertiesAll numbers extracted from JFR recordings via jdk.* eventsPrimary Result
GC Heap Summary — all boundary measurementsApache Kafka
Elasticsearch
Peak Heap & GC Pressure
RSS — Process Memory (jdk.ResidentSetSize)
Thread Allocation (jdk.ThreadAllocationStatistics)
Top Allocated Types (jdk.ObjectAllocationSample)Consistent across all runs: primitive arrays ([J, [B, [I) and
|


Closes #371
Problem
Large Java codebases (28 000+ files) caused the scanner to consume up to
90 GB of heap. The root cause was
DetectionExecutiveholding strongreferences to fully-resolved Java ASTs long after analysis completed,
combined with
DetectionStoreretaining its value/child collections forthe entire scan duration.
Solution
Eagerly release ASTs and detection state after each file:
DetectionExecutive: null outtreein afinallyblock instart()so the AST is GC-eligible immediately after rule analysis.DetectionExecutive: expose a deferred-hook lifecycle — when nodeferred hooks are registered,
releaseResources()is called eagerlyafter
emitFinding(); when deferred hooks exist,JavaBaseDetectionRuledrives cleanup via
releaseDeferredResources()inleaveFile().IStatusReporting: addonDeferredHookRegistration()default methodas a backward-compatible lifecycle callback.
DetectionStore: addrelease()to recursively cleardetectionValues,children, andactionValue.JavaBaseDetectionRule: callreleaseDeferredResources()on alldeferred executives in
leaveFile().JavaScanMemoryLogger: new utility to log JVM heap usage every Nfiles, making memory regressions visible in scan output.
Testing
24 new unit tests covering the deferred-hook state machine, recursive
store release, and memory logger behaviour.
Observed impact
Validated against Elasticsearch (28 000 Java files) and Kafka (6 000 Java
files). Peak heap dropped from >90 GB to normal scanner levels