From 3a1f72df345b017707bb0367bbf8594cb514c83f Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Sat, 15 Nov 2025 14:55:32 +0000 Subject: [PATCH 1/6] Replace IsPublicFilter with ClassNameFilter --- .../tooling/bytebuddy/outline/IsPublicFilter.java | 11 ----------- .../agent/tooling/bytebuddy/outline/TypeFactory.java | 4 +++- 2 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/IsPublicFilter.java diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/IsPublicFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/IsPublicFilter.java deleted file mode 100644 index 026d4beea6b..00000000000 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/IsPublicFilter.java +++ /dev/null @@ -1,11 +0,0 @@ -package datadog.trace.agent.tooling.bytebuddy.outline; - -import datadog.trace.agent.tooling.bytebuddy.ClassCodeFilter; -import datadog.trace.api.InstrumenterConfig; - -/** Compact filter that records public types. */ -final class IsPublicFilter extends ClassCodeFilter { - IsPublicFilter() { - super(InstrumenterConfig.get().getResolverVisibilitySize()); - } -} diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java index ba9b29438af..162f3d17555 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/outline/TypeFactory.java @@ -5,6 +5,7 @@ import static datadog.trace.bootstrap.AgentClassLoading.LOCATING_CLASS; import static net.bytebuddy.dynamic.loading.ClassLoadingStrategy.BOOTSTRAP_LOADER; +import datadog.instrument.utils.ClassNameFilter; import datadog.trace.agent.tooling.InstrumenterMetrics; import datadog.trace.agent.tooling.bytebuddy.ClassFileLocators; import datadog.trace.agent.tooling.bytebuddy.TypeInfoCache; @@ -86,7 +87,8 @@ final class TypeFactory { private static final TypeInfoCache fullTypes = new TypeInfoCache<>(InstrumenterConfig.get().getResolverTypePoolSize()); - static final IsPublicFilter isPublicFilter = new IsPublicFilter(); + static final ClassNameFilter isPublicFilter = + new ClassNameFilter(InstrumenterConfig.get().getResolverVisibilitySize()); /** Small local cache to help deduplicate lookups when matching/transforming. */ private final DDCache deferredTypes = DDCaches.newFixedSizeCache(16); From 0826fc5714f80aa297ecc03292c66b4750fc4c56 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Sat, 15 Nov 2025 15:31:45 +0000 Subject: [PATCH 2/6] Replace NoMatchFilter with ClassNameFilter --- .../tooling/bytebuddy/memoize/Memoizer.java | 3 +- .../bytebuddy/memoize/NoMatchFilter.java | 104 +++++++++--------- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java index 604912dabce..1ffb277d4c4 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/Memoizer.java @@ -2,6 +2,7 @@ import static net.bytebuddy.matcher.ElementMatchers.not; +import datadog.instrument.utils.ClassNameFilter; import datadog.trace.agent.tooling.InstrumenterMetrics; import datadog.trace.agent.tooling.bytebuddy.TypeInfoCache; import datadog.trace.agent.tooling.bytebuddy.TypeInfoCache.SharedTypeInfo; @@ -61,7 +62,7 @@ enum MatcherKind { private static final boolean namesAreUnique = InstrumenterConfig.get().isResolverNamesAreUnique(); // compact filter recording uninteresting types - private static final NoMatchFilter noMatchFilter = new NoMatchFilter(); + private static final ClassNameFilter noMatchFilter = NoMatchFilter.build(); // caches positive memoized matches private static final TypeInfoCache memos = diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java index a4f08d2edf2..d66f02b32d3 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java @@ -2,7 +2,7 @@ import static datadog.trace.util.AgentThreadFactory.AGENT_THREAD_GROUP; -import datadog.trace.agent.tooling.bytebuddy.ClassCodeFilter; +import datadog.instrument.utils.ClassNameFilter; import datadog.trace.api.Config; import datadog.trace.api.DDTraceApiInfo; import datadog.trace.api.InstrumenterConfig; @@ -20,20 +20,28 @@ import org.slf4j.LoggerFactory; /** Compact filter that records uninteresting types. */ -final class NoMatchFilter extends ClassCodeFilter { +final class NoMatchFilter { private static final Logger log = LoggerFactory.getLogger(NoMatchFilter.class); - NoMatchFilter() { - super(InstrumenterConfig.get().getResolverNoMatchesSize()); - - // seed filter from previously collected results? + public static ClassNameFilter build() { + // support persisting/restoring no-match results? Path noMatchFile = discoverNoMatchFile(); if (null != noMatchFile) { - seedNoMatchFilter(noMatchFile); + if (Files.exists(noMatchFile)) { + // restore existing filter with previously collected results + return seedNoMatchFilter(noMatchFile); + } else { + // populate filter from current run and persist at shutdown + ClassNameFilter filter = emptyNoMatchFilter(); + Runtime.getRuntime().addShutdownHook(new ShutdownHook(noMatchFile, filter)); + return filter; + } + } else { + return emptyNoMatchFilter(); } } - static Path discoverNoMatchFile() { + private static Path discoverNoMatchFile() { String cacheDir = InstrumenterConfig.get().getResolverCacheDir(); if (null == cacheDir) { return null; @@ -53,51 +61,49 @@ static Path discoverNoMatchFile() { return Paths.get(cacheDir, noMatchFilterName); } - void seedNoMatchFilter(Path noMatchFile) { - if (!Files.exists(noMatchFile)) { - Runtime.getRuntime().addShutdownHook(new ShutdownHook(noMatchFile)); - } else { - log.debug("Seeding NoMatchFilter from {}", noMatchFile); - try (DataInputStream in = - new DataInputStream(new BufferedInputStream(Files.newInputStream(noMatchFile)))) { - while (true) { - switch (in.readUTF()) { - case "dd-java-agent": - expectVersion(in, DDTraceApiInfo.VERSION); - break; - case "NoMatchFilter": - if (in.readInt() != slots.length) { - throw new IOException("filter size mismatch"); - } - for (int i = 0; i < slots.length; i++) { - slots[i] = in.readLong(); - } - return; - default: - throw new IOException("unexpected content"); - } - } - } catch (IOException e) { - if (log.isDebugEnabled()) { - log.info("Unable to seed NoMatchFilter from {}", noMatchFile, e); - } else { - log.info("Unable to seed NoMatchFilter from {}: {}", noMatchFile, e.getMessage()); + private static ClassNameFilter emptyNoMatchFilter() { + return new ClassNameFilter(InstrumenterConfig.get().getResolverNoMatchesSize()); + } + + private static ClassNameFilter seedNoMatchFilter(Path noMatchFile) { + log.debug("Seeding NoMatchFilter from {}", noMatchFile); + try (DataInputStream in = + new DataInputStream(new BufferedInputStream(Files.newInputStream(noMatchFile)))) { + while (true) { + switch (in.readUTF()) { + case "dd-java-agent": + expectVersion(in, DDTraceApiInfo.VERSION); + break; + case "NoMatchFilter": + return ClassNameFilter.readFrom(in); + default: + throw new IOException("unexpected content"); } } + } catch (Throwable e) { + if (log.isDebugEnabled()) { + log.info("Unable to seed NoMatchFilter from {}", noMatchFile, e); + } else { + log.info("Unable to seed NoMatchFilter from {}: {}", noMatchFile, e.getMessage()); + } + return emptyNoMatchFilter(); } } - void persistNoMatchFilter(Path noMatchFile) { + private static void expectVersion(DataInputStream in, String version) throws IOException { + if (!version.equals(in.readUTF())) { + throw new IOException("version mismatch"); + } + } + + static void persistNoMatchFilter(Path noMatchFile, ClassNameFilter filter) { log.debug("Persisting NoMatchFilter to {}", noMatchFile); try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(noMatchFile)))) { out.writeUTF("dd-java-agent"); out.writeUTF(DDTraceApiInfo.VERSION); out.writeUTF("NoMatchFilter"); - out.writeInt(slots.length); - for (long slot : slots) { - out.writeLong(slot); - } + filter.writeTo(out); } catch (IOException e) { if (log.isDebugEnabled()) { log.info("Unable to persist NoMatchFilter to {}", noMatchFile, e); @@ -107,23 +113,19 @@ void persistNoMatchFilter(Path noMatchFile) { } } - static void expectVersion(DataInputStream in, String version) throws IOException { - if (!version.equals(in.readUTF())) { - throw new IOException("version mismatch"); - } - } - - class ShutdownHook extends Thread { + private static final class ShutdownHook extends Thread { private final Path noMatchFile; + private final ClassNameFilter filter; - ShutdownHook(Path noMatchFile) { + ShutdownHook(Path noMatchFile, ClassNameFilter filter) { super(AGENT_THREAD_GROUP, "dd-NoMatchFilter-persist-hook"); this.noMatchFile = noMatchFile; + this.filter = filter; } @Override public void run() { - persistNoMatchFilter(noMatchFile); + persistNoMatchFilter(noMatchFile, filter); } } } From 9e879238fced02d417df4d12829abddbf3a5c230 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Sat, 15 Nov 2025 15:32:06 +0000 Subject: [PATCH 3/6] Remove now unused ClassCodeFilter --- .../tooling/bytebuddy/ClassCodeFilter.java | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/ClassCodeFilter.java diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/ClassCodeFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/ClassCodeFilter.java deleted file mode 100644 index 5f9e5c7f86b..00000000000 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/ClassCodeFilter.java +++ /dev/null @@ -1,85 +0,0 @@ -package datadog.trace.agent.tooling.bytebuddy; - -import java.util.Arrays; - -/** - * Compact filter that records class membership by their hash and short 'class-code'. - * - *

The 'class-code' includes the length of the package prefix and simple name, as well as the - * first and last characters of the simple name. These elements coupled with the hash of the full - * class name should minimize the probability of collisions without needing to store full names, - * which would otherwise make the filter overly large. - */ -public abstract class ClassCodeFilter { - - private static final int MAX_CAPACITY = 1 << 16; - private static final int MIN_CAPACITY = 1 << 8; - private static final int MAX_HASH_ATTEMPTS = 3; - - protected final long[] slots; - protected final int slotMask; - - protected ClassCodeFilter(int capacity) { - if (capacity < MIN_CAPACITY) { - capacity = MIN_CAPACITY; - } else if (capacity > MAX_CAPACITY) { - capacity = MAX_CAPACITY; - } - - // choose enough slot bits to cover the chosen capacity - slotMask = 0xFFFFFFFF >>> Integer.numberOfLeadingZeros(capacity - 1); - slots = new long[slotMask + 1]; - } - - public final boolean contains(String name) { - int hash = name.hashCode(); - for (int i = 1, h = hash; true; i++) { - long value = slots[slotMask & h]; - if (value == 0) { - return false; - } else if ((int) value == hash) { - return (int) (value >>> 32) == classCode(name); - } else if (i == MAX_HASH_ATTEMPTS) { - return false; - } - h = rehash(h); - } - } - - public final void add(String name) { - int index; - int hash = name.hashCode(); - for (int i = 1, h = hash; true; i++) { - index = slotMask & h; - if (slots[index] == 0) { - break; - } else if (i == MAX_HASH_ATTEMPTS) { - index = slotMask & hash; // overwrite original slot - break; - } - h = rehash(h); - } - slots[index] = (long) classCode(name) << 32 | 0xFFFFFFFFL & hash; - } - - public final void clear() { - Arrays.fill(slots, 0); - } - - /** - * Computes a 32-bit 'class-code' that includes the length of the package prefix and simple name, - * plus the first and last characters of the simple name (each truncated to fit into 8-bits.) - */ - private static int classCode(String name) { - int start = name.lastIndexOf('.') + 1; - int end = name.length() - 1; - int code = 0xFF & start; - code = (code << 8) | (0xFF & name.charAt(start)); - code = (code << 8) | (0xFF & name.charAt(end)); - return (code << 8) | (0xFF & (end - start)); - } - - private static int rehash(int oldHash) { - return Integer.reverseBytes(oldHash * 0x9e3775cd) * 0x9e3775cd; - } -} From 99a650a98b5d5d2ff03bd9fcbc869fe961aa88fc Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Thu, 20 Nov 2025 22:35:29 +0000 Subject: [PATCH 4/6] Header constants --- .../tooling/bytebuddy/memoize/NoMatchFilter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java index d66f02b32d3..8435dd0c0d2 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java @@ -23,6 +23,9 @@ final class NoMatchFilter { private static final Logger log = LoggerFactory.getLogger(NoMatchFilter.class); + private static final String TRACER_VERSION_HEADER = "dd-java-agent"; + private static final String NO_MATCH_FILTER_HEADER = "NoMatchFilter"; + public static ClassNameFilter build() { // support persisting/restoring no-match results? Path noMatchFile = discoverNoMatchFile(); @@ -71,10 +74,10 @@ private static ClassNameFilter seedNoMatchFilter(Path noMatchFile) { new DataInputStream(new BufferedInputStream(Files.newInputStream(noMatchFile)))) { while (true) { switch (in.readUTF()) { - case "dd-java-agent": + case TRACER_VERSION_HEADER: expectVersion(in, DDTraceApiInfo.VERSION); break; - case "NoMatchFilter": + case NO_MATCH_FILTER_HEADER: return ClassNameFilter.readFrom(in); default: throw new IOException("unexpected content"); @@ -100,9 +103,9 @@ static void persistNoMatchFilter(Path noMatchFile, ClassNameFilter filter) { log.debug("Persisting NoMatchFilter to {}", noMatchFile); try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(noMatchFile)))) { - out.writeUTF("dd-java-agent"); + out.writeUTF(TRACER_VERSION_HEADER); out.writeUTF(DDTraceApiInfo.VERSION); - out.writeUTF("NoMatchFilter"); + out.writeUTF(NO_MATCH_FILTER_HEADER); filter.writeTo(out); } catch (IOException e) { if (log.isDebugEnabled()) { From bd8eca841e6f93c4682ec803f5884b498f4ec402 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 24 Nov 2025 11:23:05 +0000 Subject: [PATCH 5/6] Document that NoMatchFilter is now a utility class --- .../trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java index 8435dd0c0d2..c35eafc1d92 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java @@ -19,13 +19,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -/** Compact filter that records uninteresting types. */ +/** Builds a persistable compact filter that records uninteresting types. */ final class NoMatchFilter { private static final Logger log = LoggerFactory.getLogger(NoMatchFilter.class); private static final String TRACER_VERSION_HEADER = "dd-java-agent"; private static final String NO_MATCH_FILTER_HEADER = "NoMatchFilter"; + private NoMatchFilter() {} + public static ClassNameFilter build() { // support persisting/restoring no-match results? Path noMatchFile = discoverNoMatchFile(); From 4ba1713c9183860f50d541a3df858ec738ef5e71 Mon Sep 17 00:00:00 2001 From: Stuart McCulloch Date: Mon, 24 Nov 2025 11:36:49 +0000 Subject: [PATCH 6/6] Catch potential security exception when checking file / installing shutdown hook --- .../bytebuddy/memoize/NoMatchFilter.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java index c35eafc1d92..ca87baa876d 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/memoize/NoMatchFilter.java @@ -29,21 +29,24 @@ final class NoMatchFilter { private NoMatchFilter() {} public static ClassNameFilter build() { - // support persisting/restoring no-match results? Path noMatchFile = discoverNoMatchFile(); if (null != noMatchFile) { - if (Files.exists(noMatchFile)) { - // restore existing filter with previously collected results - return seedNoMatchFilter(noMatchFile); - } else { - // populate filter from current run and persist at shutdown - ClassNameFilter filter = emptyNoMatchFilter(); - Runtime.getRuntime().addShutdownHook(new ShutdownHook(noMatchFile, filter)); - return filter; + // support persisting/restoring no-match results + try { + if (Files.exists(noMatchFile)) { + // restore existing filter with previously collected results + return seedNoMatchFilter(noMatchFile); + } else { + // populate filter from current run and persist at shutdown + ClassNameFilter filter = emptyNoMatchFilter(); + Runtime.getRuntime().addShutdownHook(new ShutdownHook(noMatchFile, filter)); + return filter; + } + } catch (Throwable e) { + log.debug("Unable to use NoMatchFilter at {}", noMatchFile, e); } - } else { - return emptyNoMatchFilter(); } + return emptyNoMatchFilter(); } private static Path discoverNoMatchFile() {