From ad6418f2599516ade29cd0b1d38dad398dbd99e4 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Mon, 15 Jun 2026 18:30:44 +0200 Subject: [PATCH 1/9] Refactor: Introduce `PACKAGE_PREFIX` constant to simplify repetitive code --- .../java/com/intellij/rt/debugger/agent/CaptureStorage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java index 560bfbf..35bcc8f 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java @@ -32,6 +32,7 @@ protected Deque initialValue() { ); static final double DEFAULT_OVERHEAD_PERCENT = 50; + private static final String PACKAGE_PREFIX = CaptureStorage.class.getPackage().getName(); private static OverheadDetector ourOverheadDetector = new OverheadDetector(DEFAULT_OVERHEAD_PERCENT, true); static void init(Properties properties) { @@ -650,7 +651,7 @@ private static void handleException(Throwable e) { } static boolean isAgentFrame(StackTraceElement elem) { - return elem.getClassName().startsWith(CaptureStorage.class.getPackage().getName()); + return elem.getClassName().startsWith(PACKAGE_PREFIX); } static List getCurrentStackTraceWithoutAgentFrames() { From f720eb955781eaf14edd2b323067b3d49aaaa78c Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 16 Jun 2026 17:54:46 +0200 Subject: [PATCH 2/9] IDEA-390527 Add setting for forced and soft buffer size --- .../rt/debugger/agent/LogCaptureStorage.java | 21 ++++++++++--------- .../agent/ThrowableCapacityOverhead.java | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java index 581b436..725f182 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java @@ -27,7 +27,7 @@ protected Boolean initialValue() { static final String BATCHING_ENABLED_PROPERTY = "logCaptureBatchingEnabled"; static final String BATCHING_FLUSH_PERIOD_PROPERTY = "logCaptureBatchingFlushPeriod"; static final String BATCHING_BUFFER_SIZE_PROPERTY = "logCaptureBatchingBufferSize"; - private static final long DEFAULT_BUFFER_SIZE = calculateDefaultBufferSize(); + static final String FORCE_BATCHING_BUFFER_SIZE_PROPERTY = "logCaptureForceBatchingBufferSize"; private static final int ESTIMATED_THROWABLE_BYTES = 3000; private static boolean BATCHING_ENABLED; @@ -150,16 +150,17 @@ public void run() { return true; } - static long calculateDefaultBufferSize() { - long defaultMax = 5L * 1024L * 1024L; // 5 MB - long runtimeMax = Runtime.getRuntime().maxMemory() / 100; // 1% of max heap - return Math.min(defaultMax, runtimeMax); - } - static long getBufferSize(Properties properties) { - return Long.parseLong(properties.getProperty( - BATCHING_BUFFER_SIZE_PROPERTY, - String.valueOf(DEFAULT_BUFFER_SIZE))); + String forcedBufferSize = properties.getProperty(FORCE_BATCHING_BUFFER_SIZE_PROPERTY); + if (forcedBufferSize != null) { + return Long.parseLong(forcedBufferSize); + } + String stringMaxSize = properties.getProperty(BATCHING_BUFFER_SIZE_PROPERTY); + long maxBufferSize = stringMaxSize == null + ? 5 * 1024L * 1024L // 5MB + : Long.parseLong(stringMaxSize); + long runtimeMax = Runtime.getRuntime().maxMemory() / 100; // 1% of max heap + return Math.min(maxBufferSize, runtimeMax); } private static long createNextEventId(int eventType) { diff --git a/src/test/java/com/intellij/rt/debugger/agent/ThrowableCapacityOverhead.java b/src/test/java/com/intellij/rt/debugger/agent/ThrowableCapacityOverhead.java index 0a92767..f8b6198 100644 --- a/src/test/java/com/intellij/rt/debugger/agent/ThrowableCapacityOverhead.java +++ b/src/test/java/com/intellij/rt/debugger/agent/ThrowableCapacityOverhead.java @@ -143,7 +143,7 @@ private static void measureAfterBatchZipping(int stackDepth, int repeats) throws Properties properties = new Properties(); properties.put(LogCaptureStorage.BATCHING_ENABLED_PROPERTY, "true"); properties.put(LogCaptureStorage.BATCHING_FLUSH_PERIOD_PROPERTY, "999999999"); - properties.put(LogCaptureStorage.BATCHING_BUFFER_SIZE_PROPERTY, String.valueOf(Long.MAX_VALUE)); + properties.put(LogCaptureStorage.FORCE_BATCHING_BUFFER_SIZE_PROPERTY, String.valueOf(Long.MAX_VALUE)); LogCaptureStorage.init(properties, true); recursiveCapture(stackDepth, repeats, "stdout message\n".getBytes(StandardCharsets.UTF_8)); From 979dc78a9e4d39822e8fe575a33185e8b1f3cf2f Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 16 Jun 2026 16:13:47 +0200 Subject: [PATCH 3/9] IDEA-390527 Add a flag to confirm flushing after evaluation - evaluation may fail due to object collected exception - with the introduced flag, confirmed data can be removed safely --- .../rt/debugger/agent/LogCaptureStorage.java | 50 +++++++++++++++++-- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java index 725f182..6bb38ce 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java @@ -34,9 +34,19 @@ protected Boolean initialValue() { private static long BUFFER_SIZE; private static boolean STDOUT_CAPTURE_ENABLED; - // It's used by the debugger. + // The debugger reads this value via evaluation to learn how many events have been created by the agent. + // The agent owns all writes. static final AtomicLong EVENT_COUNTER = new AtomicLong(); + /** + * The debugger writes the greatest event id that was received and decoded successfully via the {@link #packBatchedData()} call. + * The confirmation is done outside the method itself because it can be called multiple times (see javadoc). + * The confirmation is not guaranteed, it is used only as an optimization against repeated sending of the same events. + *

+ * The agent checks the flushed state based on this field and {@link #LAST_FLUSHED_EVENT_ID}. + */ + static volatile long CONFIRMED_FLUSHED_EVENT_ID = -1; + // It contains raw events that are waiting to be packed. // New ones could be added concurrently. // Raw or packed data can be flushed concurrently, leading to sending the same events multiple times. @@ -84,6 +94,20 @@ public int memoryFootprintEstimate() { public boolean markRemoved() { return removed.compareAndSet(false, true); } + + @Override + public int hashCode() { + // This override is not necessary for correctness, but it is a bit faster than Object.hashCode. + return (int) (id ^ (id >>> 32)); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof Event)) return false; + + Event event = (Event) o; + return id == event.id; + } } static class PackedBatch implements MemoryFootprintEstimate { @@ -235,6 +259,7 @@ private static void handleException(Throwable e) { * before it appears on the debugger side. The clearing happens in the periodic flush cycle. */ static String packBatchedData() throws IOException { + clearConfirmedBatches(); packRawEvents(); List packedBatchesSnapshot = new ArrayList<>(PACKED_BATCHES); if (packedBatchesSnapshot.isEmpty()) return null; @@ -272,6 +297,7 @@ private static void enqueuePackedBatch(List events) throws IOException { } private static void flushBatchedData(boolean forceOutput) throws IOException { + clearConfirmedBatches(); if (forceOutput || currentEventsEstimatedBytes() > BUFFER_SIZE) { packRawEvents(); } @@ -280,9 +306,25 @@ private static void flushBatchedData(boolean forceOutput) throws IOException { if (packedBatchesSnapshot.isEmpty()) return; outputWritten(packBatches(packedBatchesSnapshot)); - long removedBytes = removeItems(PACKED_BATCHES, packedBatchesSnapshot); + markBatchesFlushed(packedBatchesSnapshot); + } + + private static void markBatchesFlushed(Collection batches) { + long removedBytes = removeItems(PACKED_BATCHES, batches); PACKED_BATCHES_BYTES.addAndGet(-removedBytes); - setIfGreater(LAST_FLUSHED_EVENT_ID, findMaxPackedEventId(packedBatchesSnapshot)); + setIfGreater(LAST_FLUSHED_EVENT_ID, findMaxPackedEventId(batches)); + } + + private static void clearConfirmedBatches() { + long confirmedId = CONFIRMED_FLUSHED_EVENT_ID; + if (confirmedId <= LAST_FLUSHED_EVENT_ID.get()) return; + Set alreadyConfirmed = new HashSet<>(); + for (PackedBatch batch : PACKED_BATCHES) { + if (batch.lastEventId <= confirmedId) { + alreadyConfirmed.add(batch); + } + } + markBatchesFlushed(alreadyConfirmed); } private static byte[] packBytes(List events) throws IOException { @@ -388,7 +430,7 @@ private static long findMaxPackedEventId(Collection packedBatches) } private static long removeItems(ConcurrentLinkedQueue queue, Collection items) { - Set itemsToRemove = new HashSet<>(items); + Set itemsToRemove = items instanceof Set ? (Set) items : new HashSet<>(items); long removedBytes = 0; for (Iterator queueIterator = queue.iterator(); queueIterator.hasNext(); ) { From fb914e8cdb1ce16abde394c06d66be17ec3386b2 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Tue, 16 Jun 2026 16:51:54 +0200 Subject: [PATCH 4/9] IDEA-390527 Allow using the full buffer for events before zip happens --- .../rt/debugger/agent/LogCaptureStorage.java | 18 ++++++++++++++---- .../debugger/agent/LogCaptureEncodingTest.java | 11 ----------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java index 6bb38ce..d999d14 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java @@ -144,8 +144,7 @@ public static boolean init(Properties properties, boolean logCaptureEnabled) { ENABLED = true; STDOUT_CAPTURE_ENABLED = logCaptureEnabled; BATCHING_ENABLED = Boolean.parseBoolean(properties.getProperty(BATCHING_ENABLED_PROPERTY, "true")); - // Split in 2 halves for raw events and compressed batches. - BUFFER_SIZE = getBufferSize(properties) / 2; + BUFFER_SIZE = getBufferSize(properties); if (BATCHING_ENABLED && !batchingSchedulerStarted) { batchingSchedulerStarted = true; @@ -298,10 +297,21 @@ private static void enqueuePackedBatch(List events) throws IOException { private static void flushBatchedData(boolean forceOutput) throws IOException { clearConfirmedBatches(); - if (forceOutput || currentEventsEstimatedBytes() > BUFFER_SIZE) { + if (forceOutput) { packRawEvents(); + } else { + long eventsBytes = currentEventsEstimatedBytes(); + long packedBytes = PACKED_BATCHES_BYTES.get(); + if (eventsBytes + packedBytes <= BUFFER_SIZE) { + return; + } + packRawEvents(); + packedBytes = PACKED_BATCHES_BYTES.get(); + // We will zip too often if there is little space for raw events. + if (packedBytes <= BUFFER_SIZE * 9 / 10) { + return; + } } - if (!forceOutput && PACKED_BATCHES_BYTES.get() <= BUFFER_SIZE) return; List packedBatchesSnapshot = new ArrayList<>(PACKED_BATCHES); if (packedBatchesSnapshot.isEmpty()) return; diff --git a/src/test/java/com/intellij/rt/debugger/agent/LogCaptureEncodingTest.java b/src/test/java/com/intellij/rt/debugger/agent/LogCaptureEncodingTest.java index 699dac5..4cd5983 100644 --- a/src/test/java/com/intellij/rt/debugger/agent/LogCaptureEncodingTest.java +++ b/src/test/java/com/intellij/rt/debugger/agent/LogCaptureEncodingTest.java @@ -221,17 +221,6 @@ public void exceededRawEventBufferThresholdPacksEvents() throws Exception { assertEquals("packed batches use the same buffer threshold and are sent immediately", 1, LogCaptureStorage.outputWrittenDumpForTests.size()); } - @Test - public void rawEventsUseHalfOfBufferSizeAsThreshold() throws Exception { - properties.put(LogCaptureStorage.BATCHING_BUFFER_SIZE_PROPERTY, String.valueOf(2 * 3000)); - LogCaptureStorage.init(properties, true); - - capture(FileDescriptor.out, "a\n"); - - assertEquals("raw event bytes are packed after exceeding half the buffer size", 0, LogCaptureStorage.EVENTS.size()); - assertEquals(0, LogCaptureStorage.EVENTS_PAYLOAD_BYTES.get()); - } - @Test public void packBatchedDataPacksPendingRawEventsBeforeReturning() throws Exception { properties.put(LogCaptureStorage.BATCHING_BUFFER_SIZE_PROPERTY, LARGE_BUFFER_SIZE); From e73a4407f4ed9c1fa6285267a4b0fe2e41d2de95 Mon Sep 17 00:00:00 2001 From: Maksim Zuev Date: Wed, 17 Jun 2026 14:36:10 +0200 Subject: [PATCH 5/9] IDEA-390527 Use fast deflater: it's good enogh, while a bit faster --- .../intellij/rt/debugger/agent/LogCaptureStorage.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java index d999d14..80b9aeb 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java @@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; +import java.util.zip.Deflater; import java.util.zip.GZIPOutputStream; public class LogCaptureStorage { @@ -341,7 +342,7 @@ private static byte[] packBytes(List events) throws IOException { assert !events.isEmpty(); ByteArrayOutputStream bas = new ByteArrayOutputStream(); // no need to close it - try (GZIPOutputStream gos = new GZIPOutputStream(bas); + try (GZIPOutputStream gos = new FastGzipOutputStream(bas); DataOutputStream dos = new DataOutputStream(gos)) { CapturedStackDeduplicator.StackDictionary stackDictionary = CapturedStackDeduplicator.createStackDictionary(events, MAX_STACK_DEPTH); @@ -369,6 +370,13 @@ private static byte[] packBytes(List events) throws IOException { return bas.toByteArray(); } + private static class FastGzipOutputStream extends GZIPOutputStream { + FastGzipOutputStream(ByteArrayOutputStream out) throws IOException { + super(out); + def.setLevel(Deflater.BEST_SPEED); + } + } + private static byte[] packStack(List stackTrace) throws IOException { ByteArrayOutputStream bas = new ByteArrayOutputStream(); // no need to close it try (DataOutputStream dos = new DataOutputStream(bas)) { From 5c0c7f4e6270525210819064cd7e1c3594637923 Mon Sep 17 00:00:00 2001 From: Vladimir Parfinenko Date: Fri, 12 Jun 2026 13:51:30 +0200 Subject: [PATCH 6/9] IDEA: Disable logpoint inspection for the project --- .idea/inspectionProfiles/Project_Default.xml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..b809e46 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From fdd2f20ab876edba3b1a22007b316dddc74db8ff Mon Sep 17 00:00:00 2001 From: Vladimir Parfinenko Date: Fri, 12 Jun 2026 14:19:03 +0200 Subject: [PATCH 7/9] Fix some warnings, remove unnecessary suppressions --- .idea/inspectionProfiles/Project_Default.xml | 1 + .../com/intellij/rt/debugger/agent/CaptureAgent.java | 4 ++-- .../intellij/rt/debugger/agent/CaptureStorage.java | 2 -- .../agent/CollectionBreakpointInstrumentor.java | 12 ++++-------- .../intellij/rt/debugger/agent/DebuggerAgent.java | 1 - .../rt/debugger/agent/ThrowableTransformer.java | 1 - 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index b809e46..7074f4b 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/src/main/java/com/intellij/rt/debugger/agent/CaptureAgent.java b/src/main/java/com/intellij/rt/debugger/agent/CaptureAgent.java index c71e27e..c1dffc0 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/CaptureAgent.java +++ b/src/main/java/com/intellij/rt/debugger/agent/CaptureAgent.java @@ -13,7 +13,7 @@ import java.security.ProtectionDomain; import java.util.*; -@SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace", "rawtypes"}) +@SuppressWarnings("rawtypes") public final class CaptureAgent { private static Instrumentation ourInstrumentation; private static final Set mySkipped = new HashSet<>(); @@ -122,7 +122,7 @@ public byte[] transform(ClassLoader loader, static void storeClassForDebug(String className, byte[] bytes) { if (CaptureStorage.DEBUG) { try { - FileOutputStream stream = new FileOutputStream("instrumented_" + className.replaceAll("/", "_") + ".class"); + FileOutputStream stream = new FileOutputStream("instrumented_" + className.replace("/", "_") + ".class"); try { stream.write(bytes); } diff --git a/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java index 35bcc8f..3dd820c 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/CaptureStorage.java @@ -12,7 +12,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -@SuppressWarnings({"UseOfSystemOutOrSystemErr"}) public final class CaptureStorage { public static final String GENERATED_INSERT_METHOD_POSTFIX = "$$$capture"; private static final ConcurrentIdentityWeakHashMap STORAGE_GENERAL = new ConcurrentIdentityWeakHashMap<>(); @@ -646,7 +645,6 @@ public static void setEnabled(boolean enabled) { private static void handleException(Throwable e) { ENABLED = false; System.err.println("Critical error in IDEA Async Stacktraces instrumenting agent. Agent is now disabled. Please report to IDEA support:"); - //noinspection CallToPrintStackTrace e.printStackTrace(); } diff --git a/src/main/java/com/intellij/rt/debugger/agent/CollectionBreakpointInstrumentor.java b/src/main/java/com/intellij/rt/debugger/agent/CollectionBreakpointInstrumentor.java index ea220c1..c0c3656 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/CollectionBreakpointInstrumentor.java +++ b/src/main/java/com/intellij/rt/debugger/agent/CollectionBreakpointInstrumentor.java @@ -18,7 +18,7 @@ import static com.intellij.rt.debugger.agent.CaptureAgent.getInternalClsName; -@SuppressWarnings({"UseOfSystemOutOrSystemErr", "CallToPrintStackTrace", "rawtypes"}) +@SuppressWarnings("rawtypes") public class CollectionBreakpointInstrumentor { private static final String OBJECT_TYPE = "Ljava/lang/Object;"; private static final String STRING_TYPE = "Ljava/lang/String;"; @@ -139,13 +139,9 @@ private static void processFailedToInstrumentError(String className, Exception e private static void writeDebugInfo(String className, byte[] bytes) { try { System.out.println("instrumented: " + className); - FileOutputStream stream = new FileOutputStream("instrumented_" + className.replaceAll("/", "_") + ".class"); - try { - stream.write(bytes); - } - finally { - stream.close(); - } + try (FileOutputStream stream = new FileOutputStream("instrumented_" + className.replace("/", "_") + ".class")) { + stream.write(bytes); + } } catch (IOException e) { e.printStackTrace(); diff --git a/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java b/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java index fa1a7f8..faf993d 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java +++ b/src/main/java/com/intellij/rt/debugger/agent/DebuggerAgent.java @@ -14,7 +14,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; -@SuppressWarnings("UseOfSystemOutOrSystemErr") public class DebuggerAgent { private static final String KEEP_SUFFIX = "[keep]"; diff --git a/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java index c651e64..4b19161 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java @@ -85,7 +85,6 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri } catch (Exception e) { System.out.println("Capture agent: failed to instrument " + className); - //noinspection CallToPrintStackTrace e.printStackTrace(); } } From 616a5e7f5a1d88eabf0ed90aec55de5d59f02f1e Mon Sep 17 00:00:00 2001 From: Vladimir Parfinenko Date: Fri, 12 Jun 2026 14:39:00 +0200 Subject: [PATCH 8/9] ThrowableInterner: a bit of cleanup --- .../rt/debugger/agent/ThrowableInterner.java | 15 ++------ .../debugger/agent/ThrowableTransformer.java | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/ThrowableInterner.java b/src/main/java/com/intellij/rt/debugger/agent/ThrowableInterner.java index 5ac9f33..3c4de4e 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ThrowableInterner.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ThrowableInterner.java @@ -61,7 +61,7 @@ static Throwable intern(Throwable throwable, Object backtrace) { // The raw VM backtrace is captured by new Throwable(), unlike StackTraceElement[] which is // created lazily and relatively expensively by getStackTrace(). - BacktraceKey key = new BacktraceKey(throwable, backtrace); + BacktraceKey key = new BacktraceKey(backtrace); Throwable interned = INTERNED_THROWABLES.putIfAbsent(key, throwable); return interned == null ? throwable : interned; } @@ -79,20 +79,12 @@ static void disable(String message) { } private static final class BacktraceKey { - private final Class myThrowableClass; private final Object myBacktrace; private final int myHashCode; - private BacktraceKey(Throwable throwable, Object backtrace) { - myThrowableClass = throwable.getClass(); + private BacktraceKey(Object backtrace) { myBacktrace = backtrace; - myHashCode = computeHashCode(myThrowableClass, backtrace); - } - - private static int computeHashCode(Class throwableClass, Object backtrace) { - int result = throwableClass.hashCode(); - result = 31 * result + backtraceHashCode(backtrace); - return result; + myHashCode = backtraceHashCode(backtrace); } @Override @@ -102,7 +94,6 @@ public boolean equals(Object obj) { BacktraceKey key = (BacktraceKey) obj; return myHashCode == key.myHashCode && - myThrowableClass == key.myThrowableClass && backtracesEqual(myBacktrace, key.myBacktrace); } diff --git a/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java b/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java index 4b19161..41fbbaa 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java +++ b/src/main/java/com/intellij/rt/debugger/agent/ThrowableTransformer.java @@ -9,6 +9,15 @@ import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; +/** + * Achieves two extra behaviors: + *

    + *
  • each created throwable is registered in {@link CaptureStorage#captureThrowable(Throwable)} + * to be able to access the async stack trace in {@code printStackTrace} methods via {@link CaptureStorage#getAsyncStackTrace(Throwable)};
  • + *
  • the raw VM backtrace is extracted and passed to {@link ThrowableInterner} + * through {@link CaptureStorage#captureThrowableBacktrace(Object)}.
  • + *
+ */ class ThrowableTransformer implements ClassFileTransformer { static final String THROWABLE_NAME = CaptureAgent.getInternalClsName(Throwable.class); @@ -26,12 +35,18 @@ public byte[] transform(ClassLoader loader, return transformer.accept(new ClassVisitor(Opcodes.API_VERSION, transformer.writer) { private String myBacktraceFieldName; private String myBacktraceFieldDescriptor; + private boolean myMultipleBacktraceFields; @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { if (isBacktraceField(name, descriptor)) { - myBacktraceFieldName = name; - myBacktraceFieldDescriptor = descriptor; + if (myBacktraceFieldName == null) { + myBacktraceFieldName = name; + myBacktraceFieldDescriptor = descriptor; + } else { + myMultipleBacktraceFields = true; + ThrowableInterner.disable("Capture agent: cannot capture Throwable backtrace, ambiguous backtrace fields found"); + } } return super.visitField(access, name, descriptor, signature, value); } @@ -41,19 +56,21 @@ public MethodVisitor visitMethod(final int access, String name, String descripto MethodVisitor superMethodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); switch (name) { case "": - // Insert CaptureStorage calls in the end of constructors. + // Insert extra calls at the end of constructors. return new MethodVisitor(api, superMethodVisitor) { @Override public void visitInsn(int opcode) { if (opcode == Opcodes.RETURN) { - if (myBacktraceFieldName != null) { + if (myBacktraceFieldName != null && !myMultipleBacktraceFields) { + // Extract backtrace. mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitFieldInsn(Opcodes.GETFIELD, THROWABLE_NAME, myBacktraceFieldName, myBacktraceFieldDescriptor); CaptureAgent.invokeStorageMethod(mv, "captureThrowableBacktrace"); } else { - ThrowableInterner.disable("Capture agent: cannot capture Throwable backtrace, no supported backtrace field was found"); + ThrowableInterner.disable("Capture agent: cannot capture Throwable backtrace, no supported backtrace field was found before the constructor"); } + // Perform async stack trace capture. mv.visitVarInsn(Opcodes.ALOAD, 0); CaptureAgent.invokeStorageMethod(mv, "captureThrowable"); } @@ -92,7 +109,10 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri } private static boolean isBacktraceField(String name, String descriptor) { - if (!"backtrace".equals(name) && !"walkback".equals(name)) return false; - return descriptor.startsWith("L") || descriptor.startsWith("["); + // HotSpot: Object backtrace + // OpenJ9: Object walkback + // Moreover, we accept any reference type just to be ready for any variations. + return ("backtrace".equals(name) || "walkback".equals(name)) && + (descriptor.startsWith("L") || descriptor.startsWith("[")); } } From 3a3997c9a55ab401b5d5b511cddb3d8eb430d135 Mon Sep 17 00:00:00 2001 From: Vladimir Parfinenko Date: Wed, 17 Jun 2026 13:51:50 +0200 Subject: [PATCH 9/9] LogCaptureStorage: a bit of post-review --- .../agent/CapturedStackDeduplicator.java | 10 ++--- .../rt/debugger/agent/LogCaptureStorage.java | 42 ++++++++++--------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/intellij/rt/debugger/agent/CapturedStackDeduplicator.java b/src/main/java/com/intellij/rt/debugger/agent/CapturedStackDeduplicator.java index 7979099..ca26682 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/CapturedStackDeduplicator.java +++ b/src/main/java/com/intellij/rt/debugger/agent/CapturedStackDeduplicator.java @@ -29,21 +29,19 @@ private StackRef(int id, List stack) { private final List events; private final int maxStackDepth; - private final ArrayList> stacks; + private final ArrayList> stacks = new ArrayList<>(); private final int[] throwableStackIds; private final int[] capturedStackIds; - private final IdentityHashMap throwableStacks; - private final IdentityHashMap capturedStacks; + private final IdentityHashMap throwableStacks = new IdentityHashMap<>(); + private final IdentityHashMap capturedStacks = new IdentityHashMap<>(); private CapturedStackDeduplicator(List events, int maxStackDepth) { this.events = events; this.maxStackDepth = maxStackDepth; + int eventCount = events.size(); - stacks = new ArrayList<>(); throwableStackIds = new int[eventCount]; capturedStackIds = new int[eventCount]; - throwableStacks = new IdentityHashMap<>(); - capturedStacks = new IdentityHashMap<>(); } static StackDictionary createStackDictionary(List events, int maxStackDepth) { diff --git a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java index 80b9aeb..b06bbae 100644 --- a/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java +++ b/src/main/java/com/intellij/rt/debugger/agent/LogCaptureStorage.java @@ -29,7 +29,7 @@ protected Boolean initialValue() { static final String BATCHING_FLUSH_PERIOD_PROPERTY = "logCaptureBatchingFlushPeriod"; static final String BATCHING_BUFFER_SIZE_PROPERTY = "logCaptureBatchingBufferSize"; static final String FORCE_BATCHING_BUFFER_SIZE_PROPERTY = "logCaptureForceBatchingBufferSize"; - private static final int ESTIMATED_THROWABLE_BYTES = 3000; + private static final int ESTIMATED_THROWABLE_BYTES = 3000; // See the comment at the top of ThrowableCapacityOverhead private static boolean BATCHING_ENABLED; private static long BUFFER_SIZE; @@ -50,24 +50,35 @@ protected Boolean initialValue() { // It contains raw events that are waiting to be packed. // New ones could be added concurrently. - // Raw or packed data can be flushed concurrently, leading to sending the same events multiple times. - // It's ok and is handled by the debugger using IDs. static final ConcurrentLinkedQueue EVENTS = new ConcurrentLinkedQueue<>(); static final AtomicLong EVENTS_PAYLOAD_BYTES = new AtomicLong(); + + // It contains packed batches that are waiting to be sent. + // New ones could be added concurrently. static final ConcurrentLinkedQueue PACKED_BATCHES = new ConcurrentLinkedQueue<>(); static final AtomicLong PACKED_BATCHES_BYTES = new AtomicLong(); + // Raw or packed data can be flushed concurrently, leading to sending the same events multiple times. + // It's ok and is handled by the debugger using IDs. static final AtomicLong LAST_FLUSHED_EVENT_ID = new AtomicLong(-1); static final AtomicLong LAST_PACKED_EVENT_ID = new AtomicLong(-1); static final AtomicLong LAST_LOGGING_BREAKPOINT_EVENT_ID = new AtomicLong(-1); - private interface MemoryFootprintEstimate { - int memoryFootprintEstimate(); + private abstract static class MemoryFootprintEstimate { + private final AtomicBoolean removed = new AtomicBoolean(); + + public abstract int memoryFootprintEstimate(); - boolean markRemoved(); + /** Returns true if the event was removed for the first time. */ + public boolean markRemoved() { + return removed.compareAndSet(false, true); + } } - static class Event implements MemoryFootprintEstimate { + /** + * A single event. + */ + static class Event extends MemoryFootprintEstimate { public static final byte STD_OUTPUT_TYPE = 0; public static final byte LOGGING_BREAKPOINT_TYPE = 1; @@ -76,7 +87,6 @@ static class Event implements MemoryFootprintEstimate { public final byte[] payload; public final Throwable throwable; public final CaptureStorage.CapturedStack stack; - private final AtomicBoolean removed = new AtomicBoolean(); public Event(long id, byte type, byte[] payload, Throwable throwable, CaptureStorage.CapturedStack stack) { this.id = id; @@ -91,11 +101,6 @@ public int memoryFootprintEstimate() { return payload.length; } - @Override - public boolean markRemoved() { - return removed.compareAndSet(false, true); - } - @Override public int hashCode() { // This override is not necessary for correctness, but it is a bit faster than Object.hashCode. @@ -111,10 +116,12 @@ public final boolean equals(Object o) { } } - static class PackedBatch implements MemoryFootprintEstimate { + /** + * Multiple events, compressed and packed. Batch is prepared to be sent to the debugger. + */ + static class PackedBatch extends MemoryFootprintEstimate { public final byte[] data; public final long lastEventId; - private final AtomicBoolean removed = new AtomicBoolean(); public PackedBatch(byte[] data, long lastEventId) { this.data = data; @@ -125,11 +132,6 @@ public PackedBatch(byte[] data, long lastEventId) { public int memoryFootprintEstimate() { return data.length; } - - @Override - public boolean markRemoved() { - return removed.compareAndSet(false, true); - } } private static final FileDescriptor FD_OUT = FileDescriptor.out;