Skip to content

Commit 27a3f79

Browse files
committed
Support matching type by name, or collection of names
1 parent 87d9763 commit 27a3f79

File tree

3 files changed

+182
-7
lines changed

3 files changed

+182
-7
lines changed

class-match/src/main/java/datadog/instrument/classmatch/InternalMatchers.java

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99
import java.util.Arrays;
1010
import java.util.Collection;
1111
import java.util.HashMap;
12-
import java.util.HashSet;
1312
import java.util.Map;
14-
import java.util.Set;
1513
import java.util.function.Predicate;
1614

1715
/** Internally shared matchers, not part of the public API. */
@@ -31,14 +29,11 @@ static Predicate<String[]> declaresAnnotation(String type) {
3129

3230
/** Matches when at least one annotation has one of the given types. */
3331
static Predicate<String[]> declaresAnnotationOneOf(Collection<String> types) {
34-
Set<String> internalNames = new HashSet<>((int) (types.size() / 0.75f) + 1);
35-
for (String type : types) {
36-
internalNames.add(internalName(type));
37-
}
32+
InternalNames internalNames = new InternalNames(types);
3833
// note these annotations are of interest when parsing
3934
ClassFile.annotationsOfInterest(internalNames);
4035
// performance tip: capture this method-ref outside the lambda
41-
Predicate<String> annotationNamedOneOf = internalNames::contains;
36+
Predicate<String> annotationNamedOneOf = internalNames::containsType;
4237
return annotations -> anyMatch(annotations, annotationNamedOneOf);
4338
}
4439

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2025-Present Datadog, Inc.
5+
*/
6+
7+
package datadog.instrument.classmatch;
8+
9+
import static datadog.instrument.classmatch.InternalMatchers.internalName;
10+
11+
import java.util.AbstractSet;
12+
import java.util.Collection;
13+
import java.util.Iterator;
14+
import java.util.NoSuchElementException;
15+
16+
/**
17+
* Compact immutable hashtable of internal names, representing types to be matched against.
18+
*
19+
* <p>The key feature of this implementation is that it supports querying by arbitrary {@link
20+
* CharSequence}s, as long as the char sequence uses the same hash algorithm as {@link String}.
21+
*/
22+
final class InternalNames extends AbstractSet<String> {
23+
private static final int MAX_HASH_ATTEMPTS = 3;
24+
25+
private final String[] table;
26+
private final int slotMask;
27+
28+
/**
29+
* Creates a new set of internal names containing the given types of interest.
30+
*
31+
* @param types the types of interest
32+
*/
33+
InternalNames(Collection<String> types) {
34+
// attempt to hash types into a table with ~75% load factor
35+
int tableSize = Integer.max(8, types.size() * 4 / 3) - 1;
36+
int slotMask = -1 >>> Integer.numberOfLeadingZeros(tableSize);
37+
String[] table = new String[slotMask + 1];
38+
Iterator<String> itr = types.iterator();
39+
while (itr.hasNext()) {
40+
// add types one by one, watching out for unsolvable collisions
41+
if (!add(table, slotMask, internalName(itr.next()))) {
42+
// cannot add type without collision; grow table and restart additions
43+
slotMask = (slotMask << 1) + 1;
44+
table = new String[slotMask + 1];
45+
itr = types.iterator();
46+
}
47+
}
48+
this.table = table;
49+
this.slotMask = slotMask;
50+
}
51+
52+
/**
53+
* Returns {@code true} if the set of internal names contains the given type.
54+
*
55+
* @param internalName the internal name of the type
56+
* @return {@code true} if the set contains this type; otherwise {@code false}
57+
*/
58+
public boolean containsType(CharSequence internalName) {
59+
final String[] table = this.table;
60+
final int slotMask = this.slotMask;
61+
for (int i = 1, h = internalName.hashCode(); true; i++, h = rehash(h)) {
62+
String existing = table[slotMask & h];
63+
if (existing != null) {
64+
// use content-equality, not object-equality
65+
if (existing.contentEquals(internalName)) {
66+
return true;
67+
} else if (i < MAX_HASH_ATTEMPTS) {
68+
continue; // rehash and try again
69+
}
70+
}
71+
return false;
72+
}
73+
}
74+
75+
/**
76+
* Attempts to add a type to a hashtable of names, with a bounded amount of rehashing.
77+
*
78+
* @param table a hashtable of internal names
79+
* @param slotMask mask used to map hashes to slots
80+
* @param internalName the internal name of the type
81+
* @return {@code true} if the type was successfully added; otherwise {@code false}
82+
*/
83+
private static boolean add(String[] table, int slotMask, String internalName) {
84+
for (int i = 1, h = internalName.hashCode(); true; i++, h = rehash(h)) {
85+
int slot = slotMask & h;
86+
String existing = table[slot];
87+
// be prepared to de-duplicate names
88+
if (existing == null || existing.equals(internalName)) {
89+
table[slot] = internalName;
90+
return true;
91+
} else if (i < MAX_HASH_ATTEMPTS) {
92+
continue; // rehash and try again
93+
}
94+
return false;
95+
}
96+
}
97+
98+
private static int rehash(int oldHash) {
99+
return Integer.reverseBytes(oldHash * 0x9e3775cd) * 0x9e3775cd;
100+
}
101+
102+
// ----------------------------------------------------------------------------------------------
103+
// The rest of this class implements a read-only Set contract for the internal names hashtable
104+
// ----------------------------------------------------------------------------------------------
105+
106+
@Override
107+
public boolean contains(Object o) {
108+
return o instanceof CharSequence && containsType((CharSequence) o);
109+
}
110+
111+
@Override
112+
public Iterator<String> iterator() {
113+
return new Iterator<String>() {
114+
private int i = 0;
115+
116+
@Override
117+
public boolean hasNext() {
118+
for (; i < table.length; i++) {
119+
if (table[i] != null) {
120+
return true;
121+
}
122+
}
123+
return false;
124+
}
125+
126+
@Override
127+
public String next() {
128+
if (hasNext()) {
129+
return table[i++];
130+
} else {
131+
throw new NoSuchElementException();
132+
}
133+
}
134+
};
135+
}
136+
137+
@Override
138+
public int size() {
139+
// naive implementation to satisfy the AbstractSet contract; nothing calls this
140+
int size = 0;
141+
for (String s : table) {
142+
if (s != null) {
143+
size++;
144+
}
145+
}
146+
return size;
147+
}
148+
}

class-match/src/main/java/datadog/instrument/classmatch/TypeMatcher.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,44 @@
77
package datadog.instrument.classmatch;
88

99
import static datadog.instrument.classmatch.InternalMatchers.internalName;
10+
import static java.util.Arrays.asList;
1011

12+
import java.util.Collection;
1113
import java.util.function.Predicate;
1214

1315
/** Fluent-API for building type hierarchy predicates. */
1416
public interface TypeMatcher extends Predicate<CharSequence> {
1517

18+
/**
19+
* Matches when the type equals the given string.
20+
*
21+
* @param type the expected type
22+
* @return matcher of types with the same value
23+
*/
24+
static TypeMatcher type(String type) {
25+
return internalName(type)::contentEquals;
26+
}
27+
28+
/**
29+
* Matches when the type equals one of the given strings.
30+
*
31+
* @param types the expected types
32+
* @return matcher of types from the given list
33+
*/
34+
static TypeMatcher typeOneOf(String... types) {
35+
return typeOneOf(asList(types));
36+
}
37+
38+
/**
39+
* Matches when the type equals one of the given strings.
40+
*
41+
* @param types the expected types
42+
* @return matcher of types from the given list
43+
*/
44+
static TypeMatcher typeOneOf(Collection<String> types) {
45+
return new InternalNames(types)::containsType;
46+
}
47+
1648
/**
1749
* Matches when the type starts with the given prefix.
1850
*

0 commit comments

Comments
 (0)