Skip to content

Commit adb0879

Browse files
committed
Support string-like access to type-strings inside method and field descriptors while minimizing allocations
1 parent ef060d0 commit adb0879

File tree

7 files changed

+243
-45
lines changed

7 files changed

+243
-45
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
package datadog.instrument.classmatch;
88

99
import static datadog.instrument.classmatch.InternalMatchers.descriptor;
10-
import static datadog.instrument.classmatch.InternalMatchers.hasFieldType;
1110

1211
import java.util.function.IntPredicate;
1312
import java.util.function.Predicate;
@@ -75,7 +74,11 @@ default FieldMatcher type(Class<?> type) {
7574
* @return matcher of fields with a matching type
7675
*/
7776
default FieldMatcher type(TypeMatcher typeMatcher) {
78-
return and(f -> hasFieldType(f, typeMatcher));
77+
return and(
78+
f -> {
79+
TypeString fieldType = f.typeString();
80+
return fieldType != null && typeMatcher.test(fieldType);
81+
});
7982
}
8083

8184
/**

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
package datadog.instrument.classmatch;
88

9+
import javax.annotation.Nullable;
10+
911
/** Outlines a field; access modifiers, field name, descriptor. */
1012
public final class FieldOutline {
1113

@@ -23,4 +25,25 @@ public final class FieldOutline {
2325
this.fieldName = fieldName;
2426
this.descriptor = descriptor;
2527
}
28+
29+
/** Lazy cache of the field's type-string hash. */
30+
private int typeStringHash;
31+
32+
/**
33+
* Provides a {@link TypeString} of the field type for hierarchy matching purposes.
34+
*
35+
* @return type-string; {@code null} if the field type is primitive or an array
36+
*/
37+
@Nullable
38+
TypeString typeString() {
39+
if (descriptor.charAt(0) != 'L') {
40+
return null; // don't create type-strings for primitive/array types
41+
}
42+
int start = 1;
43+
int end = descriptor.length() - 1;
44+
if (typeStringHash == 0) {
45+
typeStringHash = TypeString.computeHash(descriptor, start, end);
46+
}
47+
return new TypeString(descriptor, start, end, typeStringHash);
48+
}
2649
}

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

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -54,41 +54,6 @@ static boolean hasParamDescriptor(MethodOutline method, int paramIndex, String p
5454
&& method.descriptor.startsWith(paramDescriptor, boundaries[paramIndex - 1]);
5555
}
5656

57-
/** Does the method's descriptor contain a matching parameter type at the given index? */
58-
static boolean hasParamType(MethodOutline method, int paramIndex, TypeMatcher typeMatcher) {
59-
// boundaries covers start of second parameter, to start of return descriptor
60-
int[] boundaries = method.descriptorBoundaries();
61-
if (paramIndex >= boundaries.length) { // ignore return descriptor boundary
62-
return false;
63-
}
64-
// first parameter always starts at 1, for the rest check the boundary list
65-
int from = paramIndex == 0 ? 1 : boundaries[paramIndex - 1];
66-
String descriptor = method.descriptor;
67-
// extract type from "L...;" descriptor string, ignore primitive/array types
68-
return descriptor.charAt(from) == 'L'
69-
&& typeMatcher.test(descriptor.substring(from + 1, boundaries[paramIndex] - 1));
70-
}
71-
72-
/** Does the method's descriptor contain a matching return type? */
73-
static boolean hasReturnType(MethodOutline method, TypeMatcher typeMatcher) {
74-
// boundaries covers start of second parameter, to start of return descriptor
75-
int[] boundaries = method.descriptorBoundaries();
76-
// return type starts at 2 if no parameters, otherwise check last boundary
77-
int from = boundaries.length == 0 ? 2 : boundaries[boundaries.length - 1];
78-
String descriptor = method.descriptor;
79-
// extract type from "L...;" descriptor string, ignore primitive/array types
80-
return descriptor.charAt(from) == 'L'
81-
&& typeMatcher.test(descriptor.substring(from + 1, descriptor.length() - 1));
82-
}
83-
84-
/** Does the field's descriptor contain a matching type? */
85-
static boolean hasFieldType(FieldOutline field, TypeMatcher typeMatcher) {
86-
String descriptor = field.descriptor;
87-
// extract type from "L...;" descriptor string, ignore primitive/array types
88-
return descriptor.charAt(0) == 'L'
89-
&& typeMatcher.test(descriptor.substring(1, descriptor.length() - 1));
90-
}
91-
9257
/** Returns the descriptor for the given type. */
9358
static String descriptor(String type) {
9459
if (type.endsWith("[]")) {

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
import static datadog.instrument.classmatch.InternalMatchers.declaresAnnotationOneOf;
1414
import static datadog.instrument.classmatch.InternalMatchers.descriptor;
1515
import static datadog.instrument.classmatch.InternalMatchers.hasParamDescriptor;
16-
import static datadog.instrument.classmatch.InternalMatchers.hasParamType;
17-
import static datadog.instrument.classmatch.InternalMatchers.hasReturnType;
1816
import static java.util.Arrays.asList;
1917

2018
import java.util.Collection;
@@ -101,7 +99,7 @@ default MethodMatcher parameters(int paramCount) {
10199
if (paramCount == 0) {
102100
return noParameters();
103101
} else {
104-
return and(m -> m.descriptorBoundaries().length == paramCount);
102+
return and(m -> m.parameterCount() == paramCount);
105103
}
106104
}
107105

@@ -175,7 +173,11 @@ default MethodMatcher parameter(int paramIndex, Class<?> paramType) {
175173
* @return matcher of methods with a matching parameter type
176174
*/
177175
default MethodMatcher parameter(int paramIndex, TypeMatcher typeMatcher) {
178-
return and(m -> hasParamType(m, paramIndex, typeMatcher));
176+
return and(
177+
m -> {
178+
TypeString paramType = m.parameterTypeString(paramIndex);
179+
return paramType != null && typeMatcher.test(paramType);
180+
});
179181
}
180182

181183
/**
@@ -207,7 +209,11 @@ default MethodMatcher returning(Class<?> returnType) {
207209
* @return matcher of methods with a matching return type
208210
*/
209211
default MethodMatcher returning(TypeMatcher typeMatcher) {
210-
return and(m -> hasReturnType(m, typeMatcher));
212+
return and(
213+
m -> {
214+
TypeString returnType = m.returnTypeString();
215+
return returnType != null && typeMatcher.test(returnType);
216+
});
211217
}
212218

213219
/**

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.util.BitSet;
1212
import java.util.List;
13+
import javax.annotation.Nullable;
1314

1415
/** Outlines a method; access modifiers, method name, descriptor, annotations. */
1516
public final class MethodOutline {
@@ -26,9 +27,6 @@ public final class MethodOutline {
2627
/** Internal names of annotations declared on this method. */
2728
final String[] annotations;
2829

29-
/** Lazy cache of boundaries between each parameter/return descriptor. */
30-
private int[] descriptorBoundaries;
31-
3230
/**
3331
* @return internal names of annotations declared on this method
3432
*/
@@ -43,8 +41,57 @@ public List<String> annotations() {
4341
this.annotations = annotations;
4442
}
4543

44+
/**
45+
* @return number of method parameters
46+
*/
47+
int parameterCount() {
48+
return descriptorBoundaries().length;
49+
}
50+
51+
/**
52+
* Provides a {@link TypeString} of the indexed parameter type for hierarchy matching purposes.
53+
*
54+
* @param paramIndex the parameter index
55+
* @return type-string; {@code null} if the parameter type is primitive, an array, or missing
56+
*/
57+
@Nullable
58+
TypeString parameterTypeString(int paramIndex) {
59+
int[] boundaries = descriptorBoundaries();
60+
if (paramIndex >= boundaries.length) {
61+
return null; // method doesn't have enough parameters to match
62+
}
63+
// earliest potential param type can be found at index 2 "(L...;"
64+
int start = paramIndex == 0 ? 2 : boundaries[paramIndex - 1] + 1;
65+
if (descriptor.charAt(start - 1) != 'L') {
66+
return null; // don't create type-strings for primitive/array types
67+
}
68+
int end = boundaries[paramIndex] - 1;
69+
return new TypeString(descriptor, start, end, getHash(paramIndex, start, end));
70+
}
71+
72+
/**
73+
* Provides a {@link TypeString} of the return type for hierarchy matching purposes.
74+
*
75+
* @return type-string; {@code null} if the return type is primitive, an array, or missing
76+
*/
77+
@Nullable
78+
TypeString returnTypeString() {
79+
int[] boundaries = descriptorBoundaries();
80+
int returnIndex = boundaries.length;
81+
// earliest potential return type can be found at index 3 "()L...;"
82+
int start = returnIndex == 0 ? 3 : boundaries[returnIndex - 1] + 1;
83+
if (descriptor.charAt(start - 1) != 'L') {
84+
return null; // don't create type-strings for primitive/array/void types
85+
}
86+
int end = descriptor.length() - 1;
87+
return new TypeString(descriptor, start, end, getHash(returnIndex, start, end));
88+
}
89+
4690
private static final int[] NO_BOUNDARIES = {};
4791

92+
/** Lazy cache of boundaries between each parameter/return descriptor. */
93+
private int[] descriptorBoundaries;
94+
4895
/**
4996
* Returns the boundaries between each parameter/return descriptor in the method descriptor.
5097
*
@@ -101,4 +148,20 @@ private static int[] parseBoundaries(String descriptor) {
101148
}
102149
return boundaries;
103150
}
151+
152+
/** Lazy cache of hashes for each parameter/return type-string. */
153+
private int[] typeStringHashes;
154+
155+
/** Gets a previously cached {@link TypeString} hash; otherwise computes and caches a new hash. */
156+
private int getHash(int typeStringIndex, int start, int end) {
157+
if (typeStringHashes == null) {
158+
// allow for one hash per parameter, plus one for return type
159+
typeStringHashes = new int[descriptorBoundaries.length + 1];
160+
}
161+
int hash = typeStringHashes[typeStringIndex];
162+
if (hash == 0) {
163+
typeStringHashes[typeStringIndex] = hash = TypeString.computeHash(descriptor, start, end);
164+
}
165+
return hash;
166+
}
104167
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,35 @@
66

77
package datadog.instrument.classmatch;
88

9+
import static datadog.instrument.classmatch.InternalMatchers.internalName;
10+
911
import java.util.function.Predicate;
1012

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

16+
/**
17+
* Matches when the type starts with the given prefix.
18+
*
19+
* @param prefix the expected prefix
20+
* @return matcher of types starting with the prefix
21+
*/
22+
static TypeMatcher typeStartsWith(String prefix) {
23+
String internalPrefix = internalName(prefix);
24+
return cs -> TypeString.startsWith(cs, internalPrefix);
25+
}
26+
27+
/**
28+
* Matches when the type ends with the given suffix.
29+
*
30+
* @param suffix the expected suffix
31+
* @return matcher of types ending with the suffix
32+
*/
33+
static TypeMatcher typeEndsWith(String suffix) {
34+
String internalSuffix = internalName(suffix);
35+
return cs -> TypeString.endsWith(cs, internalSuffix);
36+
}
37+
1438
/**
1539
* Conjunction of this matcher AND another.
1640
*
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
/**
10+
* Provides {@link String}-like access to type-strings inside method and field descriptors without
11+
* allocating a completely separate {@link String}. Hashes are precomputed because type-strings are
12+
* expected to be used as lookup keys; it also makes the hashes easier to cache and re-use.
13+
*/
14+
final class TypeString implements CharSequence {
15+
16+
private final String descriptor;
17+
private final int offset;
18+
private final int len;
19+
private final int hash;
20+
21+
/**
22+
* Computes the {@link String}-hash for part of the descriptor.
23+
*
24+
* @param descriptor the field/method descriptor
25+
* @param start the start index
26+
* @param end the end index
27+
* @return string-hash for the selected range
28+
*/
29+
static int computeHash(String descriptor, int start, int end) {
30+
int h = 0;
31+
for (int i = start; i < end; i++) {
32+
h = 31 * h + descriptor.charAt(i);
33+
}
34+
return h;
35+
}
36+
37+
/**
38+
* Creates a {@link TypeString} window onto a field or method descriptor.
39+
*
40+
* @param descriptor the field/method descriptor
41+
* @param start the start index
42+
* @param end the end index
43+
* @param hash the computed hash
44+
*/
45+
TypeString(String descriptor, int start, int end, int hash) {
46+
this.descriptor = descriptor;
47+
this.offset = start;
48+
this.len = end - start;
49+
this.hash = hash;
50+
}
51+
52+
@Override
53+
public int length() {
54+
return len;
55+
}
56+
57+
@Override
58+
public char charAt(int index) {
59+
return descriptor.charAt(offset + index);
60+
}
61+
62+
@Override
63+
public CharSequence subSequence(int start, int end) {
64+
throw new UnsupportedOperationException("Partial TypeStrings not allowed");
65+
}
66+
67+
@Override
68+
public int hashCode() {
69+
return hash;
70+
}
71+
72+
@Override
73+
public boolean equals(Object o) {
74+
if (o instanceof CharSequence) {
75+
CharSequence cs = (CharSequence) o;
76+
if (len != cs.length()) {
77+
return false;
78+
}
79+
for (int i = offset, j = 0; j < len; i++, j++) {
80+
if (descriptor.charAt(i) != cs.charAt(j)) {
81+
return false;
82+
}
83+
}
84+
return true;
85+
} else {
86+
return false;
87+
}
88+
}
89+
90+
@Override
91+
public String toString() {
92+
return descriptor.substring(offset, offset + len);
93+
}
94+
95+
/** Avoids string allocation if the char sequence being tested is a {@link TypeString}. */
96+
static boolean startsWith(CharSequence cs, String prefix) {
97+
if (cs instanceof TypeString) {
98+
TypeString ts = (TypeString) cs;
99+
return ts.descriptor.startsWith(prefix, ts.offset);
100+
} else {
101+
return cs.toString().startsWith(prefix);
102+
}
103+
}
104+
105+
/** Avoids string allocation if the char sequence being tested is a {@link TypeString}. */
106+
static boolean endsWith(CharSequence cs, String suffix) {
107+
if (cs instanceof TypeString) {
108+
TypeString ts = (TypeString) cs;
109+
return ts.descriptor.startsWith(suffix, (ts.offset + ts.len) - suffix.length());
110+
} else {
111+
return cs.toString().endsWith(suffix);
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)