Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package datadog.instrument.classmatch;

import static datadog.instrument.classmatch.InternalMatchers.internalName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.openjdk.jmh.annotations.Mode.AverageTime;

import datadog.instrument.testing.SampleClasses;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

/** Reports time per corpus class matched against every sampled group, without short-circuiting. */
@State(Scope.Thread)
@BenchmarkMode(AverageTime)
@OutputTimeUnit(NANOSECONDS)
@Fork(3)
@Warmup(iterations = 4, time = 1)
@Measurement(iterations = 6, time = 1)
public class InternalNamesBenchmark {
private static final int CLASS_COUNT = 33957;

@Param({"STRING", "TYPE_STRING"})
public String queryType;

private InternalNames[] groups;
private CharSequence[] queries;

@Setup
public void setup() throws IOException {
String[] classNames =
SampleClasses.loadClassNames().stream()
.map(InternalMatchers::internalName)
.toArray(String[]::new);
queries = Arrays.stream(classNames).map(this::query).toArray(CharSequence[]::new);
// OperationsPerInvocation must match the shared corpus size.
if (queries.length != CLASS_COUNT) {
throw new IllegalStateException("Update CLASS_COUNT for the shared corpus");
}

List<InternalNames> sets = new ArrayList<>();
long matches = 0;
try (InputStream in =
requireNonNull(
InternalNamesBenchmark.class.getResourceAsStream("type-name-groups.csv"),
"type-name-groups.csv");
BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
String[] columns = line.split(",");
InternalNames names = new InternalNames(Arrays.asList(columns).subList(1, columns.length));
sets.add(names);
Set<String> expected = new HashSet<>();
for (int i = 1; i < columns.length; i++) {
expected.add(internalName(columns[i]));
}
// Validate hits even for group members absent from the shared corpus.
for (String name : expected) {
if (!names.containsType(query(name))) {
throw new IllegalStateException("Missing group member: " + name);
}
}
for (int i = 0; i < queries.length; i++) {
CharSequence query = queries[i];
boolean hit = expected.contains(classNames[i]);
if (names.containsType(query) != hit) {
throw new IllegalStateException("Incorrect corpus membership: " + query);
}
matches += hit ? 1 : 0;
}
}
}
if (sets.isEmpty()) {
throw new IllegalStateException("No sampled groups");
}
groups = sets.toArray(new InternalNames[0]);
System.out.printf(
"Corpus: %d classes, %d groups, %d hits%n", queries.length, groups.length, matches);
}

private CharSequence query(String name) {
// Both query forms have their hashes computed outside measurement.
int hash = name.hashCode();
if (queryType.equals("STRING")) {
return name;
}
String descriptor = "L" + name + ";";
return new TypeString(descriptor, 1, descriptor.length() - 1, hash);
}

@Benchmark
@OperationsPerInvocation(CLASS_COUNT)
public long matchAllGroups() {
long matches = 0;
for (CharSequence query : queries) {
for (InternalNames names : groups) {
matches += names.containsType(query) ? 1 : 0;
}
}
return matches;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Literal dotted namedOneOf/namedNoneOf groups of two or more names from dd-trace-java.
# Computed arguments excluded; duplicate groups and argument order retained.
namedOneOf,scala.collection.Seq,scala.collection.immutable.Seq
namedOneOf,software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage,software.amazon.awssdk.core.internal.http.pipeline.stages.MakeAsyncHttpRequestStage
namedOneOf,com.hazelcast.client.impl.HazelcastClientInstanceImpl,com.hazelcast.client.impl.clientside.HazelcastClientInstanceImpl
namedOneOf,org.hibernate.Session,org.hibernate.StatelessSession
namedOneOf,org.hibernate.Session,org.hibernate.StatelessSession
namedOneOf,org.hibernate.Session,org.hibernate.StatelessSession
namedOneOf,com.netflix.hystrix.HystrixCommand,com.netflix.hystrix.HystrixObservableCommand
namedOneOf,org.apache.ignite.cache.query.Query,org.apache.ignite.cache.query.SqlFieldsQuery
namedOneOf,io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe,io.grpc.netty.shaded.io.netty.channel.nio.AbstractNioChannel$AbstractNioUnsafe,io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe,io.grpc.netty.shaded.io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe,io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe,io.grpc.netty.shaded.io.netty.channel.kqueue.AbstractKQueueChannel$AbstractKQueueUnsafe
namedOneOf,reactor.core.scheduler.SchedulerTask,reactor.core.scheduler.WorkerTask
namedOneOf,io.netty.util.concurrent.GlobalEventExecutor,io.grpc.netty.shaded.io.netty.util.concurrent.GlobalEventExecutor,com.couchbase.client.deps.io.netty.util.concurrent.GlobalEventExecutor
namedOneOf,io.netty.handler.timeout.IdleStateHandler,io.grpc.netty.shaded.io.netty.handler.timeout.IdleStateHandler
namedOneOf,io.netty.channel.ChannelHandlerContext,io.grpc.netty.shaded.io.netty.channel.ChannelHandlerContext
namedOneOf,org.elasticsearch.common.util.concurrent.EsThreadPoolExecutor,org.opensearch.common.util.concurrent.OpenSearchThreadPoolExecutor
namedNoneOf,java.lang.ClassLoader,com.ibm.oti.vm.BootstrapClassLoader
namedOneOf,org.eclipse.jetty.client.api.Request,org.eclipse.jetty.client.HttpRequest
namedOneOf,org.eclipse.jetty.client.api.Request,org.eclipse.jetty.client.HttpRequest
namedOneOf,io.micronaut.http.MutableHttpResponse,io.micronaut.http.HttpResponse
namedOneOf,reactor.core.publisher.Mono,reactor.core.publisher.Flux
namedOneOf,reactor.core.publisher.Mono,reactor.core.publisher.Flux
namedOneOf,redis.Request,redis.ActorRequest,redis.BufferedRequest,redis.RoundRobinPoolRequest
namedOneOf,com.ibm.ws.session.HttpSessionFacade,org.apache.catalina.session.StandardSessionFacade
namedOneOf,com.twilio.base.Creator,com.twilio.base.Deleter,com.twilio.base.Fetcher,com.twilio.base.Reader,com.twilio.base.Updater
namedOneOf,com.twilio.base.Creator,com.twilio.base.Deleter,com.twilio.base.Fetcher,com.twilio.base.Reader,com.twilio.base.Updater
namedOneOf,io.vertx.redis.client.impl.RedisConnectionImpl,io.vertx.redis.client.impl.RedisStandaloneConnection
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,41 @@
import java.util.NoSuchElementException;

/**
* Compact immutable hashtable of internal names, representing types to be matched against.
* Immutable set of internal names using linear probing. A parallel array of cached hashes lets
* probes skip strings whose hashes differ.
*
* <p>The key feature of this implementation is that it supports querying by arbitrary {@link
* CharSequence}s, as long as the char sequence uses the same hash algorithm as {@link String}.
* <p>Queries may be any {@link CharSequence} with a {@link String}-compatible hash code.
*/
final class InternalNames extends AbstractSet<String> {
private static final int MAX_HASH_ATTEMPTS = 3;

private final String[] table;
private final int slotMask;
private final int[] hashes;
private final String[] table;

/**
* Creates a new set of internal names containing the given types of interest.
*
* @param types the types of interest
*/
InternalNames(Collection<String> types) {
// attempt to hash types into a table with ~75% load factor
int tableSize = Math.max(8, types.size() * 4 / 3) - 1;
int slotMask = -1 >>> Integer.numberOfLeadingZeros(tableSize);
String[] table = new String[slotMask + 1];
Iterator<String> itr = types.iterator();
while (itr.hasNext()) {
// add types one by one, watching out for unsolvable collisions
if (!add(table, slotMask, internalName(itr.next()))) {
// cannot add type without collision; grow table and restart additions
slotMask = (slotMask << 1) + 1;
table = new String[slotMask + 1];
itr = types.iterator();
// At most 50% full, leaving empty slots to terminate probes.
int tableSize = Math.max(8, types.size() * 2) - 1;

this.slotMask = -1 >>> Integer.numberOfLeadingZeros(tableSize);
this.hashes = new int[slotMask + 1];
this.table = new String[slotMask + 1];

for (String type : types) {
final String name = internalName(type);
final int h = hash(name.hashCode());

int hashed, slot = h & slotMask;
while ((hashed = hashes[slot]) != 0 && (hashed != h || !table[slot].equals(name))) {
slot = (slot + 1) & slotMask;
}

hashes[slot] = h;
table[slot] = name;
}
this.table = table;
this.slotMask = slotMask;
}

/**
Expand All @@ -56,47 +58,24 @@ final class InternalNames extends AbstractSet<String> {
* @return {@code true} if the set contains this type; otherwise {@code false}
*/
public boolean containsType(CharSequence internalName) {
final String[] table = this.table;
final int h = hash(internalName.hashCode());

final int slotMask = this.slotMask;
for (int i = 1, h = internalName.hashCode(); true; i++, h = rehash(h)) {
String existing = table[slotMask & h];
if (existing != null) {
// use content-equality, not object-equality
if (existing.contentEquals(internalName)) {
return true;
} else if (i < MAX_HASH_ATTEMPTS) {
continue; // rehash and try again
}
}
return false;
}
}
final int[] hashes = this.hashes;
final String[] table = this.table;

/**
* Attempts to add a type to a hashtable of names, with a bounded amount of rehashing.
*
* @param table a hashtable of internal names
* @param slotMask mask used to map hashes to slots
* @param internalName the internal name of the type
* @return {@code true} if the type was successfully added; otherwise {@code false}
*/
private static boolean add(String[] table, int slotMask, String internalName) {
for (int i = 1, h = internalName.hashCode(); true; i++, h = rehash(h)) {
int slot = slotMask & h;
String existing = table[slot];
// be prepared to de-duplicate names
if (existing == null || existing.equals(internalName)) {
table[slot] = internalName;
int hashed, slot = h & slotMask;
while ((hashed = hashes[slot]) != 0) {
if (hashed == h && table[slot].contentEquals(internalName)) {
return true;
} else if (i < MAX_HASH_ATTEMPTS) {
continue; // rehash and try again
}
return false;
slot = (slot + 1) & slotMask;
}
return false;
}

private static int rehash(int oldHash) {
return Integer.reverseBytes(oldHash * 0x9e3775cd) * 0x9e3775cd;
private static int hash(int h) {
return h != 0 ? h : 0xDD06; // reserve 0 for "no hash"
}

// ----------------------------------------------------------------------------------------------
Expand Down
Loading
Loading