This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1abf54de77f0 CAMEL-26812: camel-sanitizer - keep the stack of a 
callback as StackWalker frames, and make the stack trace only for a report 
(#29892)
1abf54de77f0 is described below

commit 1abf54de77f02d552138b63077b7e1472fb7d58e
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Nov 15 02:26:16 2026 +0530

    CAMEL-26812: camel-sanitizer - keep the stack of a callback as StackWalker 
frames, and make the stack trace only for a report (#29892)
    
    Rule R1 takes the stack of the thread at the first callback of every step
    call, so that a second callback can be reported with both stacks.
    Sanitizer.trace() turned up to stackDepth frames (default 32) into
    StackTraceElements right away, which is about half of the cost, and
    almost every trace is dropped without a report.
    
    The trace now keeps the StackFrames of the StackWalker, and the report
    turns them into StackTraceElements when it shows them (with file and
    line, as before). A trace of what was thrown keeps its
    StackTraceElements, as before. The stacks of R2 (handover and run of a
    commit) get the same saving. Measured with the JMH benchmark of
    CAMEL-26751 (route of 6 steps, pendingTimeout 0): 63.5 us to 31.0 us per
    message with stackDepth 32; a split of 4 parts with a seda hop: 115.7 us
    to 62.0 us. The sanitizer page says what the stack costs, with the
    numbers for stackDepth 0, 8 and 32.
    
    CAMEL-26812: camel-sanitizer - review: check each stack on its own, two 
typed fields for the frames, ratios in the doc
    
    Review of #29892 by Claus Ibsen.
    
    - SanitizerRecordTest: the text is split on the second label, so each
      stack is checked on its own, and the second stack must not hold the
      frame of firstCallback.
    - Sanitizer.Trace keeps the StackWalker frames and the stack trace of
      what was thrown in two typed fields, without a cast.
    - sanitizer.adoc: the benchmark is given as ratios, measured on one
      machine.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/catalog/docs/sanitizer.adoc   |  6 ++++
 .../camel-sanitizer/src/main/docs/sanitizer.adoc   |  6 ++++
 .../java/org/apache/camel/sanitizer/Sanitizer.java | 41 ++++++++++++++++------
 .../camel/sanitizer/SanitizerRecordTest.java       | 24 +++++++++++++
 4 files changed, 66 insertions(+), 11 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sanitizer.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sanitizer.adoc
index da7a7170eec0..430beefb0f8f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sanitizer.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/sanitizer.adoc
@@ -321,6 +321,12 @@ parallel split, which share one lock of the table (and of 
the records of R2). It
 pending warning, and the calls still pending then.
 |===
 
+The stack of each first callback is what the sanitizer costs most: it is taken 
for every call of a step, in case a
+second callback comes. In a benchmark on one machine, a route of 6 steps took 
about 15 times as long per message with
+the default `stackDepth` of `32` as with `0`, and about half as long with `8` 
as with `32` (with `0` it was still
+slower than without the sanitizer). A lower `stackDepth` makes a test suite 
faster and changes the timing of the races
+less; the report then shows fewer frames.
+
 == In a test
 
 Add the dependency in test scope:
diff --git a/components/camel-sanitizer/src/main/docs/sanitizer.adoc 
b/components/camel-sanitizer/src/main/docs/sanitizer.adoc
index da7a7170eec0..430beefb0f8f 100644
--- a/components/camel-sanitizer/src/main/docs/sanitizer.adoc
+++ b/components/camel-sanitizer/src/main/docs/sanitizer.adoc
@@ -321,6 +321,12 @@ parallel split, which share one lock of the table (and of 
the records of R2). It
 pending warning, and the calls still pending then.
 |===
 
+The stack of each first callback is what the sanitizer costs most: it is taken 
for every call of a step, in case a
+second callback comes. In a benchmark on one machine, a route of 6 steps took 
about 15 times as long per message with
+the default `stackDepth` of `32` as with `0`, and about half as long with `8` 
as with `32` (with `0` it was still
+slower than without the sanitizer). A lower `stackDepth` makes a test suite 
faster and changes the timing of the races
+less; the report then shows fewer frames.
+
 == In a test
 
 Add the dependency in test scope:
diff --git 
a/components/camel-sanitizer/src/main/java/org/apache/camel/sanitizer/Sanitizer.java
 
b/components/camel-sanitizer/src/main/java/org/apache/camel/sanitizer/Sanitizer.java
index 8b5392393aed..a08dfc964cbb 100644
--- 
a/components/camel-sanitizer/src/main/java/org/apache/camel/sanitizer/Sanitizer.java
+++ 
b/components/camel-sanitizer/src/main/java/org/apache/camel/sanitizer/Sanitizer.java
@@ -284,13 +284,14 @@ public final class Sanitizer {
     Trace trace() {
         String thread = Thread.currentThread().getName();
         if (stackDepth <= 0) {
-            return new Trace(thread, null);
+            return new Trace(thread, null, null);
         }
-        StackTraceElement[] frames = WALKER.walk(s -> s
+        // the frames are kept as they are, and turned into StackTraceElements 
(file and line) only when a report shows
+        // them: almost every trace is dropped without a report, and the 
conversion is about half of the cost
+        StackWalker.StackFrame[] frames = WALKER.walk(s -> s
                 .dropWhile(f -> isOwnFrame(f.getClassName()))
                 .limit(stackDepth)
-                .map(StackWalker.StackFrame::toStackTraceElement)
-                .toArray(StackTraceElement[]::new));
+                .toArray(StackWalker.StackFrame[]::new));
         return new Trace(thread, frames);
     }
 
@@ -300,12 +301,12 @@ public final class Sanitizer {
     Trace trace(Throwable thrown) {
         String thread = Thread.currentThread().getName();
         if (stackDepth <= 0) {
-            return new Trace(thread, null);
+            return new Trace(thread, null, null);
         }
         StackTraceElement[] all = thrown.getStackTrace();
         StackTraceElement[] frames = new 
StackTraceElement[Math.min(stackDepth, all.length)];
         System.arraycopy(all, 0, frames, 0, frames.length);
-        return new Trace(thread, frames);
+        return Trace.ofThrown(thread, frames);
     }
 
     private static boolean isOwnFrame(String className) {
@@ -1043,17 +1044,35 @@ public final class Sanitizer {
     static final class Trace {
 
         private final String thread;
-        private final StackTraceElement[] frames;
+        // the stack of the thread, as the StackFrames of a StackWalker, which 
are turned into StackTraceElements only
+        // when shown (null when the trace is of what was thrown)
+        private final StackWalker.StackFrame[] walked;
+        // the stack of what was thrown (null when the trace is of the thread)
+        private final StackTraceElement[] thrown;
+
+        Trace(String thread, StackWalker.StackFrame[] walked) {
+            this(thread, walked, null);
+        }
+
+        static Trace ofThrown(String thread, StackTraceElement[] thrown) {
+            return new Trace(thread, null, thrown);
+        }
 
-        Trace(String thread, StackTraceElement[] frames) {
+        Trace(String thread, StackWalker.StackFrame[] walked, 
StackTraceElement[] thrown) {
             this.thread = thread;
-            this.frames = frames;
+            this.walked = walked;
+            this.thrown = thrown;
         }
 
         void appendTo(StringBuilder sb, String what) {
             sb.append("  ").append(what).append(": thread 
").append(thread).append('\n');
-            if (frames != null) {
-                for (StackTraceElement f : frames) {
+            if (walked != null) {
+                for (StackWalker.StackFrame f : walked) {
+                    sb.append("      at 
").append(f.toStackTraceElement()).append('\n');
+                }
+            }
+            if (thrown != null) {
+                for (StackTraceElement f : thrown) {
                     sb.append("      at ").append(f).append('\n');
                 }
             }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/sanitizer/SanitizerRecordTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/sanitizer/SanitizerRecordTest.java
index e4b110ae4bfd..227f1ff14063 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/sanitizer/SanitizerRecordTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/sanitizer/SanitizerRecordTest.java
@@ -148,6 +148,30 @@ class SanitizerRecordTest {
         assertTrue(text.contains("at " + SanitizerRecordTest.class.getName()), 
text);
     }
 
+    @Test
+    void aReportShowsTheFramesOfBothCallbacksWithTheirFileAndLine() {
+        // the stack of the first callback is kept as the frames of the 
StackWalker and turned into file and line only
+        // when the report is made (CAMEL-26812): the report must still show 
them as a stack trace does
+        Sanitizer sanitizer = new Sanitizer(context, 32, session);
+        SanitizerStepListener.StepCall call = new 
SanitizerStepListener.StepCall(sanitizer, "ex", "r", "s");
+        firstCallback(call);
+        call.callback();
+        String text = sanitizer.getViolations().get(0).getText();
+        String file = "(" + SanitizerRecordTest.class.getSimpleName() + 
".java:";
+        // each stack is checked on its own: the first one also holds the 
frame of this method
+        int second = text.indexOf("second call of the callback");
+        assertTrue(second > 0, text);
+        String name = SanitizerRecordTest.class.getName();
+        assertTrue(text.substring(0, second).contains("at " + name + 
".firstCallback" + file), text);
+        assertTrue(text.substring(second)
+                .contains("at " + name + 
".aReportShowsTheFramesOfBothCallbacksWithTheirFileAndLine" + file), text);
+        assertFalse(text.substring(second).contains(".firstCallback("), text);
+    }
+
+    private static void firstCallback(SanitizerStepListener.StepCall call) {
+        call.callback();
+    }
+
     private static void await(CyclicBarrier barrier, Runnable action) {
         try {
             barrier.await(10, TimeUnit.SECONDS);

Reply via email to