perf(engine): detach call-stack ASTs at leaveFile + self-contained heap harness#481
Merged
Conversation
dd2375c to
9f31637
Compare
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…Verifier) Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…y (retained) cases Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…EW_ARRAY arg) Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…ariant Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…ay + reporting Record calls retained during their file's analysis (same-file hook detections resolve and report through the live context); at leaveFile, swap each detachable call for a pre-built tree-free DetachedCall so the file's AST is collected while cross-file matching continues from the snapshot. Cross-file detached detections produce CBOM nodes and report SonarQube issues via SonarComponents (no AST pin). Removes the redundant visitedTreeObjects set (per-bucket dedup instead). Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Scan only the call-stack bucket matching a hook's method name instead of every bucket; fall back to a full scan for ANY/multi-name matchers. Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…loak scan) Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…ting Measured: plugin footprint ~28MB (~1% of ~2.9GB floor); CBOM nodes negligible (~25KB), call-stack dominates plugin term (15.6MB, linear). Floor growth is SonarQube/ECJ baseline, not detectedNodes. Heap track effectively closed; eligibility filter reclassified to CPU/throughput. Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
…nsistency Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
buildDetachedCall snapshotted its invoked-object/parameter IType keys with MatchContext.build(false, rule) (record context), but a DetachedCall is only ever matched in hook context via MethodMatcher.matchKeys. Under isHookContext=false the translation selects subtype-permissive matching (is || isSubtypeOf) instead of the exact is() the live retained-call path uses, so cross-file matches accepted subtypes the same-file path rejects (false positives) and the outcome depended on file visitation order. Snapshot with MatchContext.createForHookContext() so detached matching reproduces the live retained-call path exactly. Also: - gate the heap-attribution debug log behind isDebugEnabled() so the full CallContextStats.from call-stack traversal does not run on every production scan when DEBUG is off. - clarify ILanguageSupport.callContextStats is diagnostics-only and not part of the detection contract. Signed-off-by: Nicklas Körtge <Nicklas.Koertge1@ibm.com>
9f31637 to
7c20aea
Compare
The AST-detach branch narrowed CallStackAgent.onNewHookSubscription to fetch only the method-name-keyed bucket (bucketsToScan) instead of scanning all buckets. That narrowing is shared by every language, so document and guard its safety for Python and Go: - Python: bucketing is exercised within-file only (within-file wrapper-hook tests already pass, proving key alignment). No scan-level cross-file symbol resolution exists, so the narrowing can't drop cross-file detections. Added a two-file CrossFileHookResolveTest, @disabled like ResolveImportedStructTest. - Go: GoDetectionEngine only populates the call stack and never registers hooks, so onNewHookSubscription/bucketsToScan is unreachable for Go; the GoVerifier harness is single-file only. Added a @disabled CrossFileHookDetachTest that documents both blockers. Both tests are forward guards: enable them if either language gains cross-file or hook-based resolution.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stops
com.ibm.engine.callstackfrom pinning whole-file ASTs for the entire scan, and adds a self-contained harness that proves it.On large scans the call-stack retained a
CallContextper matched invocation for the whole project; each held a live sonar-javaTree, and anyTreepins its file's full AST (parent chain →CompilationUnitTree). Measured on a near-full keycloak scan: ~179kCallContexts driving the heap to ~7 GB, growing linearly and unbounded.What changed
AST-detach mechanism (detach at
leaveFile, not at record time)CallContextis now a sealed interface:RetainedCall(tree, scanContext, detachedForm?)/DetachedCall(tree-free: match keys + pre-resolvedArgSnapshots + AST-freeDetachedScanContext).NEW_ARRAYarg) get aDetachedCallpre-built while the file is live; atleaveFilethe retained entry is swapped for it, so the file's AST becomes GC-eligible while cross-file matching continues from the snapshot.RetainedCall, so their SonarQube issues are unaffected. Cross-file detached detections report via the internalSonarComponents.reportIssue(shared per-scan, pins no AST).Performance/heap harness (this session)
CallContextStats+callContextStats()accessor threadedCallStackAgent → Handler → ILanguageSupport → JavaLanguageSupport— read-only introspection of retained-with-tree vs. detached counts.CryptoCorpusGenerator— generates N synthetic cross-file crypto wrapper/caller unit pairs (JCA APIs) into a temp dir.CallStackHeapPerfTest(@Tag("performance"), excluded from the default build via surefireexcludedGroups) — generates → compiles to a classpath dir → scans viaCheckVerifierso types resolve and detections fire → assertsdetachedRatiohigh andretainedWithTreebounded (deterministic; no heap-MB flake) → reports heap delta + wall-time for manual soak runs.Run it:
mvn test -pl java -DexcludedGroups= -Dtest=CallStackHeapPerfTest(add-Dperf.corpus.files=3000for a heavy soak).Verification
feat/callstack-ast-detach(200 files):retainedWithTree=0 detached=367 ratio=1.000— every recorded call detached.detachCallsForFileneutralized, the harness fails (retainedWithTree=300 ratio=0.000), confirming the assertion catches AST-pinning regressions.Design / plan docs
docs/superpowers/specs/2026-07-06-callstack-heap-perf-harness-design.mddocs/superpowers/plans/2026-07-06-callstack-heap-perf-harness.mddocs/superpowers/plans/2026-07-05-callstack-hooks-heap-reduction.md(the ~7 GB measurement)Note
This does not reproduce the full-project ~7 GB number in CI — that remains the documented manual
mvn sonar:sonarkeycloak route. The harness targets the engine mechanism at a scaled, deterministic level.