Merge pull request #1189 from DataDog/dougqh/type-caching2
Revised type cache
This commit is contained in:
commit
53d32b4324
|
@ -291,7 +291,10 @@ class MuzzlePlugin implements Plugin<Project> {
|
||||||
doLast {
|
doLast {
|
||||||
final ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject, toolingProject)
|
final ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject, toolingProject)
|
||||||
def ccl = Thread.currentThread().contextClassLoader
|
def ccl = Thread.currentThread().contextClassLoader
|
||||||
def bogusLoader = new SecureClassLoader()
|
def bogusLoader = new SecureClassLoader() {
|
||||||
|
@Override
|
||||||
|
String toString() { return "bogus" }
|
||||||
|
}
|
||||||
Thread.currentThread().contextClassLoader = bogusLoader
|
Thread.currentThread().contextClassLoader = bogusLoader
|
||||||
final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
|
final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -16,7 +16,7 @@ public class AgentTooling {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final DDLocationStrategy LOCATION_STRATEGY = new DDLocationStrategy();
|
private static final DDLocationStrategy LOCATION_STRATEGY = new DDLocationStrategy();
|
||||||
private static final DDCachingPoolStrategy POOL_STRATEGY = new DDCachingPoolStrategy(CLEANER);
|
private static final DDCachingPoolStrategy POOL_STRATEGY = new DDCachingPoolStrategy();
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
// Only need to trigger static initializers for now.
|
// Only need to trigger static initializers for now.
|
||||||
|
|
|
@ -20,6 +20,7 @@ class Cleaner {
|
||||||
final Thread thread = new Thread(r, "dd-cleaner");
|
final Thread thread = new Thread(r, "dd-cleaner");
|
||||||
thread.setDaemon(true);
|
thread.setDaemon(true);
|
||||||
thread.setPriority(Thread.MIN_PRIORITY);
|
thread.setPriority(Thread.MIN_PRIORITY);
|
||||||
|
thread.setContextClassLoader(null);
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,146 +1,235 @@
|
||||||
package datadog.trace.agent.tooling;
|
package datadog.trace.agent.tooling;
|
||||||
|
|
||||||
import static datadog.trace.agent.tooling.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER;
|
|
||||||
import static datadog.trace.agent.tooling.ClassLoaderMatcher.skipClassLoader;
|
|
||||||
import static net.bytebuddy.agent.builder.AgentBuilder.PoolStrategy;
|
import static net.bytebuddy.agent.builder.AgentBuilder.PoolStrategy;
|
||||||
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import datadog.trace.bootstrap.WeakMap;
|
import java.lang.ref.WeakReference;
|
||||||
import java.security.SecureClassLoader;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import net.bytebuddy.description.type.TypeDescription;
|
import net.bytebuddy.description.type.TypeDescription;
|
||||||
import net.bytebuddy.dynamic.ClassFileLocator;
|
import net.bytebuddy.dynamic.ClassFileLocator;
|
||||||
import net.bytebuddy.pool.TypePool;
|
import net.bytebuddy.pool.TypePool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Pool strategy.
|
* NEW (Jan 2020) Custom Pool strategy.
|
||||||
*
|
*
|
||||||
* <p>Here we are using WeakMap.Provider as the backing ClassLoader -> CacheProvider lookup.
|
* <ul>
|
||||||
|
* Uses a Guava Cache directly...
|
||||||
|
* <li>better control over locking than WeakMap.Provider
|
||||||
|
* <li>provides direct control over concurrency level
|
||||||
|
* <li>initial and maximum capacity
|
||||||
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>We also use our bootstrap proxy when matching against the bootstrap loader.
|
* <ul>
|
||||||
|
* There two core parts to the cache...
|
||||||
|
* <li>a cache of ClassLoader to WeakReference<ClassLoader>
|
||||||
|
* <li>a single cache of TypeResolutions for all ClassLoaders - keyed by a custom composite key of
|
||||||
|
* ClassLoader & class name
|
||||||
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>The CacheProvider is a custom implementation that uses guava's cache to expire and limit size.
|
* <p>This design was chosen to create a single limited size cache that can be adjusted for the
|
||||||
|
* entire application -- without having to create a large number of WeakReference objects.
|
||||||
*
|
*
|
||||||
* <p>By evicting from the cache we are able to reduce the memory overhead of the agent for apps
|
* <p>Eviction is handled almost entirely through a size restriction; however, softValues are still
|
||||||
* that have many classes.
|
* used as a further safeguard.
|
||||||
*
|
|
||||||
* <p>See eviction policy below.
|
|
||||||
*/
|
*/
|
||||||
public class DDCachingPoolStrategy
|
@Slf4j
|
||||||
implements PoolStrategy, WeakMap.ValueSupplier<ClassLoader, TypePool.CacheProvider> {
|
public class DDCachingPoolStrategy implements PoolStrategy {
|
||||||
|
// Many things are package visible for testing purposes --
|
||||||
|
// others to avoid creation of synthetic accessors
|
||||||
|
|
||||||
// Need this because we can't put null into the typePoolCache map.
|
static final int CONCURRENCY_LEVEL = 8;
|
||||||
private static final ClassLoader BOOTSTRAP_CLASSLOADER_PLACEHOLDER =
|
static final int LOADER_CAPACITY = 64;
|
||||||
new SecureClassLoader(null) {};
|
static final int TYPE_CAPACITY = 64;
|
||||||
|
|
||||||
private final WeakMap<ClassLoader, TypePool.CacheProvider> typePoolCache =
|
static final int BOOTSTRAP_HASH = 0;
|
||||||
WeakMap.Provider.newWeakMap();
|
|
||||||
private final Cleaner cleaner;
|
|
||||||
|
|
||||||
public DDCachingPoolStrategy(final Cleaner cleaner) {
|
/**
|
||||||
this.cleaner = cleaner;
|
* Cache of recent ClassLoader WeakReferences; used to...
|
||||||
}
|
*
|
||||||
|
* <ul>
|
||||||
@Override
|
* <li>Reduced number of WeakReferences created
|
||||||
public TypePool typePool(final ClassFileLocator classFileLocator, final ClassLoader classLoader) {
|
* <li>Allow for quick fast path equivalence check of composite keys
|
||||||
final ClassLoader key =
|
* </ul>
|
||||||
BOOTSTRAP_CLASSLOADER == classLoader ? BOOTSTRAP_CLASSLOADER_PLACEHOLDER : classLoader;
|
*/
|
||||||
final TypePool.CacheProvider cache = typePoolCache.computeIfAbsent(key, this);
|
final Cache<ClassLoader, WeakReference<ClassLoader>> loaderRefCache =
|
||||||
|
|
||||||
return new TypePool.Default.WithLazyResolution(
|
|
||||||
cache, classFileLocator, TypePool.Default.ReaderMode.FAST);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TypePool.CacheProvider get(final ClassLoader key) {
|
|
||||||
if (BOOTSTRAP_CLASSLOADER_PLACEHOLDER != key && skipClassLoader().matches(key)) {
|
|
||||||
// Don't bother creating a cache for a classloader that won't match.
|
|
||||||
// (avoiding a lot of DelegatingClassLoader instances)
|
|
||||||
// This is primarily an optimization.
|
|
||||||
return TypePool.CacheProvider.NoOp.INSTANCE;
|
|
||||||
} else {
|
|
||||||
return EvictingCacheProvider.withObjectType(cleaner, 1, TimeUnit.MINUTES);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class EvictingCacheProvider implements TypePool.CacheProvider {
|
|
||||||
|
|
||||||
/** A map containing all cached resolutions by their names. */
|
|
||||||
private final Cache<String, TypePool.Resolution> cache;
|
|
||||||
|
|
||||||
/** Creates a new simple cache. */
|
|
||||||
private EvictingCacheProvider(
|
|
||||||
final Cleaner cleaner, final long expireDuration, final TimeUnit unit) {
|
|
||||||
cache =
|
|
||||||
CacheBuilder.newBuilder()
|
CacheBuilder.newBuilder()
|
||||||
.initialCapacity(100) // Per classloader, so we want a small default.
|
.weakKeys()
|
||||||
.maximumSize(5000)
|
.concurrencyLevel(CONCURRENCY_LEVEL)
|
||||||
.softValues()
|
.initialCapacity(LOADER_CAPACITY / 2)
|
||||||
.expireAfterAccess(expireDuration, unit)
|
.maximumSize(LOADER_CAPACITY)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
/*
|
/**
|
||||||
* The cache only does cleanup on occasional reads and writes.
|
* Single shared Type.Resolution cache -- uses a composite key -- conceptually of loader & name
|
||||||
* We want to ensure this happens more regularly, so we schedule a thread to do run cleanup manually.
|
|
||||||
*/
|
*/
|
||||||
cleaner.scheduleCleaning(cache, CacheCleaner.CLEANER, expireDuration, unit);
|
final Cache<TypeCacheKey, TypePool.Resolution> sharedResolutionCache =
|
||||||
|
CacheBuilder.newBuilder()
|
||||||
|
.softValues()
|
||||||
|
.concurrencyLevel(CONCURRENCY_LEVEL)
|
||||||
|
.initialCapacity(TYPE_CAPACITY)
|
||||||
|
.maximumSize(TYPE_CAPACITY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
/** Fast path for bootstrap */
|
||||||
|
final SharedResolutionCacheAdapter bootstrapCacheProvider =
|
||||||
|
new SharedResolutionCacheAdapter(BOOTSTRAP_HASH, null, sharedResolutionCache);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final TypePool typePool(
|
||||||
|
final ClassFileLocator classFileLocator, final ClassLoader classLoader) {
|
||||||
|
if (classLoader == null) {
|
||||||
|
return createCachingTypePool(bootstrapCacheProvider, classFileLocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static EvictingCacheProvider withObjectType(
|
WeakReference<ClassLoader> loaderRef = loaderRefCache.getIfPresent(classLoader);
|
||||||
final Cleaner cleaner, final long expireDuration, final TimeUnit unit) {
|
|
||||||
final EvictingCacheProvider cacheProvider =
|
if (loaderRef == null) {
|
||||||
new EvictingCacheProvider(cleaner, expireDuration, unit);
|
loaderRef = new WeakReference<>(classLoader);
|
||||||
cacheProvider.register(
|
loaderRefCache.put(classLoader, loaderRef);
|
||||||
Object.class.getName(), new TypePool.Resolution.Simple(TypeDescription.OBJECT));
|
}
|
||||||
return cacheProvider;
|
|
||||||
|
int loaderHash = classLoader.hashCode();
|
||||||
|
return createCachingTypePool(loaderHash, loaderRef, classFileLocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TypePool.CacheProvider createCacheProvider(
|
||||||
|
final int loaderHash, final WeakReference<ClassLoader> loaderRef) {
|
||||||
|
return new SharedResolutionCacheAdapter(loaderHash, loaderRef, sharedResolutionCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TypePool createCachingTypePool(
|
||||||
|
final int loaderHash,
|
||||||
|
final WeakReference<ClassLoader> loaderRef,
|
||||||
|
final ClassFileLocator classFileLocator) {
|
||||||
|
return new TypePool.Default.WithLazyResolution(
|
||||||
|
createCacheProvider(loaderHash, loaderRef),
|
||||||
|
classFileLocator,
|
||||||
|
TypePool.Default.ReaderMode.FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final TypePool createCachingTypePool(
|
||||||
|
final TypePool.CacheProvider cacheProvider, final ClassFileLocator classFileLocator) {
|
||||||
|
return new TypePool.Default.WithLazyResolution(
|
||||||
|
cacheProvider, classFileLocator, TypePool.Default.ReaderMode.FAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
final long approximateSize() {
|
||||||
|
return sharedResolutionCache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeCacheKey is key for the sharedResolutionCache. Conceptually, it is a mix of ClassLoader &
|
||||||
|
* class name.
|
||||||
|
*
|
||||||
|
* <p>For efficiency & GC purposes, it is actually composed of loaderHash &
|
||||||
|
* WeakReference<ClassLoader>
|
||||||
|
*
|
||||||
|
* <p>The loaderHash exists to avoid calling get & strengthening the Reference.
|
||||||
|
*/
|
||||||
|
static final class TypeCacheKey {
|
||||||
|
private final int loaderHash;
|
||||||
|
private final WeakReference<ClassLoader> loaderRef;
|
||||||
|
private final String className;
|
||||||
|
|
||||||
|
private final int hashCode;
|
||||||
|
|
||||||
|
TypeCacheKey(
|
||||||
|
final int loaderHash, final WeakReference<ClassLoader> loaderRef, final String className) {
|
||||||
|
this.loaderHash = loaderHash;
|
||||||
|
this.loaderRef = loaderRef;
|
||||||
|
this.className = className;
|
||||||
|
|
||||||
|
hashCode = (int) (31 * this.loaderHash) ^ className.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TypePool.Resolution find(final String name) {
|
public final int hashCode() {
|
||||||
return cache.getIfPresent(name);
|
return hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TypePool.Resolution register(final String name, final TypePool.Resolution resolution) {
|
public boolean equals(final Object obj) {
|
||||||
try {
|
if (!(obj instanceof TypeCacheKey)) return false;
|
||||||
return cache.get(name, new ResolutionProvider(resolution));
|
|
||||||
} catch (final ExecutionException e) {
|
TypeCacheKey that = (TypeCacheKey) obj;
|
||||||
|
|
||||||
|
if (loaderHash != that.loaderHash) return false;
|
||||||
|
|
||||||
|
// Fastpath loaderRef equivalence -- works because of WeakReference cache used
|
||||||
|
// Also covers the bootstrap null loaderRef case
|
||||||
|
if (loaderRef == that.loaderRef) {
|
||||||
|
// still need to check name
|
||||||
|
return className.equals(that.className);
|
||||||
|
} else if (className.equals(that.className)) {
|
||||||
|
// need to perform a deeper loader check -- requires calling Reference.get
|
||||||
|
// which can strengthen the Reference, so deliberately done last
|
||||||
|
|
||||||
|
// If either reference has gone null, they aren't considered equivalent
|
||||||
|
// Technically, this is a bit of violation of equals semantics, since
|
||||||
|
// two equivalent references can become not equivalent.
|
||||||
|
|
||||||
|
// In this case, it is fine because that means the ClassLoader is no
|
||||||
|
// longer live, so the entries will never match anyway and will fall
|
||||||
|
// out of the cache.
|
||||||
|
ClassLoader thisLoader = loaderRef.get();
|
||||||
|
if (thisLoader == null) return false;
|
||||||
|
|
||||||
|
ClassLoader thatLoader = that.loaderRef.get();
|
||||||
|
if (thatLoader == null) return false;
|
||||||
|
|
||||||
|
return (thisLoader == thatLoader);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class SharedResolutionCacheAdapter implements TypePool.CacheProvider {
|
||||||
|
private static final String OBJECT_NAME = "java.lang.Object";
|
||||||
|
private static final TypePool.Resolution OBJECT_RESOLUTION =
|
||||||
|
new TypePool.Resolution.Simple(TypeDescription.OBJECT);
|
||||||
|
|
||||||
|
private final int loaderHash;
|
||||||
|
private final WeakReference<ClassLoader> loaderRef;
|
||||||
|
private final Cache<TypeCacheKey, TypePool.Resolution> sharedResolutionCache;
|
||||||
|
|
||||||
|
SharedResolutionCacheAdapter(
|
||||||
|
final int loaderHash,
|
||||||
|
final WeakReference<ClassLoader> loaderRef,
|
||||||
|
final Cache<TypeCacheKey, TypePool.Resolution> sharedResolutionCache) {
|
||||||
|
this.loaderHash = loaderHash;
|
||||||
|
this.loaderRef = loaderRef;
|
||||||
|
this.sharedResolutionCache = sharedResolutionCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TypePool.Resolution find(final String className) {
|
||||||
|
TypePool.Resolution existingResolution =
|
||||||
|
sharedResolutionCache.getIfPresent(new TypeCacheKey(loaderHash, loaderRef, className));
|
||||||
|
if (existingResolution != null) return existingResolution;
|
||||||
|
|
||||||
|
if (OBJECT_NAME.equals(className)) {
|
||||||
|
return OBJECT_RESOLUTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TypePool.Resolution register(
|
||||||
|
final String className, final TypePool.Resolution resolution) {
|
||||||
|
if (OBJECT_NAME.equals(className)) {
|
||||||
return resolution;
|
return resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharedResolutionCache.put(new TypeCacheKey(loaderHash, loaderRef, className), resolution);
|
||||||
|
return resolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
cache.invalidateAll();
|
// Allowing the high-level eviction policy make the clearing decisions
|
||||||
}
|
|
||||||
|
|
||||||
public long size() {
|
|
||||||
return cache.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CacheCleaner implements Cleaner.Adapter<Cache> {
|
|
||||||
private static final CacheCleaner CLEANER = new CacheCleaner();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clean(final Cache target) {
|
|
||||||
target.cleanUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ResolutionProvider implements Callable<TypePool.Resolution> {
|
|
||||||
private final TypePool.Resolution value;
|
|
||||||
|
|
||||||
private ResolutionProvider(final TypePool.Resolution value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TypePool.Resolution call() {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,221 @@
|
||||||
|
package datadog.trace.agent.tooling
|
||||||
|
|
||||||
|
import datadog.trace.util.test.DDSpecification
|
||||||
|
import net.bytebuddy.description.type.TypeDescription
|
||||||
|
import net.bytebuddy.dynamic.ClassFileLocator
|
||||||
|
import net.bytebuddy.pool.TypePool
|
||||||
|
import spock.lang.Timeout
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
@Timeout(5)
|
||||||
|
class CacheProviderTest extends DDSpecification {
|
||||||
|
def "key bootstrap equivalence"() {
|
||||||
|
// def loader = null
|
||||||
|
def loaderHash = DDCachingPoolStrategy.BOOTSTRAP_HASH
|
||||||
|
def loaderRef = null
|
||||||
|
|
||||||
|
def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo")
|
||||||
|
def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo")
|
||||||
|
|
||||||
|
expect:
|
||||||
|
key1.hashCode() == key2.hashCode()
|
||||||
|
key1.equals(key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "key same ref equivalence"() {
|
||||||
|
setup:
|
||||||
|
def loader = newClassLoader()
|
||||||
|
def loaderHash = loader.hashCode()
|
||||||
|
def loaderRef = new WeakReference<ClassLoader>(loader)
|
||||||
|
|
||||||
|
def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo")
|
||||||
|
def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo")
|
||||||
|
|
||||||
|
expect:
|
||||||
|
key1.hashCode() == key2.hashCode()
|
||||||
|
key1.equals(key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "key different ref equivalence"() {
|
||||||
|
setup:
|
||||||
|
def loader = newClassLoader()
|
||||||
|
def loaderHash = loader.hashCode()
|
||||||
|
def loaderRef1 = new WeakReference<ClassLoader>(loader)
|
||||||
|
def loaderRef2 = new WeakReference<ClassLoader>(loader)
|
||||||
|
|
||||||
|
def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef1, "foo")
|
||||||
|
def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef2, "foo")
|
||||||
|
|
||||||
|
expect:
|
||||||
|
loaderRef1 != loaderRef2
|
||||||
|
|
||||||
|
key1.hashCode() == key2.hashCode()
|
||||||
|
key1.equals(key2)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "key mismatch -- same loader - diff name"() {
|
||||||
|
setup:
|
||||||
|
def loader = newClassLoader()
|
||||||
|
def loaderHash = loader.hashCode()
|
||||||
|
def loaderRef = new WeakReference<ClassLoader>(loader)
|
||||||
|
def fooKey = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo")
|
||||||
|
def barKey = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "bar")
|
||||||
|
|
||||||
|
expect:
|
||||||
|
// not strictly guaranteed -- but important for performance
|
||||||
|
fooKey.hashCode() != barKey.hashCode()
|
||||||
|
!fooKey.equals(barKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "key mismatch -- same name - diff loader"() {
|
||||||
|
setup:
|
||||||
|
def loader1 = newClassLoader()
|
||||||
|
def loader1Hash = loader1.hashCode()
|
||||||
|
def loaderRef1 = new WeakReference<ClassLoader>(loader1)
|
||||||
|
|
||||||
|
def loader2 = newClassLoader()
|
||||||
|
def loader2Hash = loader2.hashCode()
|
||||||
|
def loaderRef2 = new WeakReference<ClassLoader>(loader2)
|
||||||
|
|
||||||
|
def fooKey1 = new DDCachingPoolStrategy.TypeCacheKey(loader1Hash, loaderRef1, "foo")
|
||||||
|
def fooKey2 = new DDCachingPoolStrategy.TypeCacheKey(loader2Hash, loaderRef2, "foo")
|
||||||
|
|
||||||
|
expect:
|
||||||
|
// not strictly guaranteed -- but important for performance
|
||||||
|
fooKey1.hashCode() != fooKey2.hashCode()
|
||||||
|
!fooKey1.equals(fooKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
def "test basic caching"() {
|
||||||
|
setup:
|
||||||
|
def poolStrat = new DDCachingPoolStrategy()
|
||||||
|
|
||||||
|
def loader = newClassLoader()
|
||||||
|
def loaderHash = loader.hashCode()
|
||||||
|
def loaderRef = new WeakReference<ClassLoader>(loader)
|
||||||
|
|
||||||
|
def cacheProvider = poolStrat.createCacheProvider(loaderHash, loaderRef)
|
||||||
|
|
||||||
|
when:
|
||||||
|
cacheProvider.register("foo", new TypePool.Resolution.Simple(TypeDescription.VOID))
|
||||||
|
|
||||||
|
then:
|
||||||
|
// not strictly guaranteed, but fine for this test
|
||||||
|
cacheProvider.find("foo") != null
|
||||||
|
poolStrat.approximateSize() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def "test loader equivalence"() {
|
||||||
|
setup:
|
||||||
|
def poolStrat = new DDCachingPoolStrategy()
|
||||||
|
|
||||||
|
def loader1 = newClassLoader()
|
||||||
|
def loaderHash1 = loader1.hashCode()
|
||||||
|
def loaderRef1A = new WeakReference<ClassLoader>(loader1)
|
||||||
|
def loaderRef1B = new WeakReference<ClassLoader>(loader1)
|
||||||
|
|
||||||
|
def cacheProvider1A = poolStrat.createCacheProvider(loaderHash1, loaderRef1A)
|
||||||
|
def cacheProvider1B = poolStrat.createCacheProvider(loaderHash1, loaderRef1B)
|
||||||
|
|
||||||
|
when:
|
||||||
|
cacheProvider1A.register("foo", newVoid())
|
||||||
|
|
||||||
|
then:
|
||||||
|
// not strictly guaranteed, but fine for this test
|
||||||
|
cacheProvider1A.find("foo") != null
|
||||||
|
cacheProvider1B.find("foo") != null
|
||||||
|
|
||||||
|
cacheProvider1A.find("foo").is(cacheProvider1B.find("foo"))
|
||||||
|
poolStrat.approximateSize() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
def "test loader separation"() {
|
||||||
|
setup:
|
||||||
|
def poolStrat = new DDCachingPoolStrategy()
|
||||||
|
|
||||||
|
def loader1 = newClassLoader()
|
||||||
|
def loaderHash1 = loader1.hashCode()
|
||||||
|
def loaderRef1 = new WeakReference<ClassLoader>(loader1)
|
||||||
|
|
||||||
|
def loader2 = newClassLoader()
|
||||||
|
def loaderHash2 = loader2.hashCode()
|
||||||
|
def loaderRef2 = new WeakReference<ClassLoader>(loader2)
|
||||||
|
|
||||||
|
def cacheProvider1 = poolStrat.createCacheProvider(loaderHash1, loaderRef1)
|
||||||
|
def cacheProvider2 = poolStrat.createCacheProvider(loaderHash2, loaderRef2)
|
||||||
|
|
||||||
|
when:
|
||||||
|
cacheProvider1.register("foo", newVoid())
|
||||||
|
cacheProvider2.register("foo", newVoid())
|
||||||
|
|
||||||
|
then:
|
||||||
|
// not strictly guaranteed, but fine for this test
|
||||||
|
cacheProvider1.find("foo") != null
|
||||||
|
cacheProvider2.find("foo") != null
|
||||||
|
|
||||||
|
!cacheProvider1.find("foo").is(cacheProvider2.find("foo"))
|
||||||
|
poolStrat.approximateSize() == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
def "test capacity"() {
|
||||||
|
setup:
|
||||||
|
def poolStrat = new DDCachingPoolStrategy()
|
||||||
|
def capacity = DDCachingPoolStrategy.TYPE_CAPACITY
|
||||||
|
|
||||||
|
def loader1 = newClassLoader()
|
||||||
|
def loaderHash1 = loader1.hashCode()
|
||||||
|
def loaderRef1 = new WeakReference<ClassLoader>(loader1)
|
||||||
|
|
||||||
|
def loader2 = newClassLoader()
|
||||||
|
def loaderHash2 = loader2.hashCode()
|
||||||
|
def loaderRef2 = new WeakReference<ClassLoader>(loader2)
|
||||||
|
|
||||||
|
def cacheProvider1 = poolStrat.createCacheProvider(loaderHash1, loaderRef1)
|
||||||
|
def cacheProvider2 = poolStrat.createCacheProvider(loaderHash2, loaderRef2)
|
||||||
|
|
||||||
|
def id = 0
|
||||||
|
|
||||||
|
when:
|
||||||
|
(capacity / 2).times {
|
||||||
|
id += 1
|
||||||
|
cacheProvider1.register("foo${id}", newVoid())
|
||||||
|
cacheProvider2.register("foo${id}", newVoid())
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
// cache will start to proactively free slots & size calc is approximate
|
||||||
|
poolStrat.approximateSize() > 0.8 * capacity
|
||||||
|
|
||||||
|
when:
|
||||||
|
10.times {
|
||||||
|
id += 1
|
||||||
|
cacheProvider1.register("foo${id}", newVoid())
|
||||||
|
cacheProvider2.register("foo${id}", newVoid())
|
||||||
|
}
|
||||||
|
|
||||||
|
then:
|
||||||
|
// cache will start to proactively free slots & size calc is approximate
|
||||||
|
poolStrat.approximateSize() > 0.8 * capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
static newVoid() {
|
||||||
|
return new TypePool.Resolution.Simple(TypeDescription.VOID)
|
||||||
|
}
|
||||||
|
|
||||||
|
static newClassLoader() {
|
||||||
|
return new URLClassLoader([] as URL[], (ClassLoader)null)
|
||||||
|
}
|
||||||
|
|
||||||
|
static newLocator() {
|
||||||
|
return new ClassFileLocator() {
|
||||||
|
@Override
|
||||||
|
ClassFileLocator.Resolution locate(String name) throws IOException {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void close() throws IOException {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,102 +0,0 @@
|
||||||
package datadog.trace.agent.tooling
|
|
||||||
|
|
||||||
import datadog.trace.util.gc.GCUtils
|
|
||||||
import datadog.trace.util.test.DDSpecification
|
|
||||||
import net.bytebuddy.description.type.TypeDescription
|
|
||||||
import net.bytebuddy.pool.TypePool
|
|
||||||
import spock.lang.Timeout
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.concurrent.atomic.AtomicReference
|
|
||||||
|
|
||||||
import static datadog.trace.agent.tooling.AgentTooling.CLEANER
|
|
||||||
|
|
||||||
@Timeout(5)
|
|
||||||
class EvictingCacheProviderTest extends DDSpecification {
|
|
||||||
|
|
||||||
def "test provider"() {
|
|
||||||
setup:
|
|
||||||
def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, 2, TimeUnit.MINUTES)
|
|
||||||
|
|
||||||
expect:
|
|
||||||
provider.size() == 0
|
|
||||||
provider.find(className) == null
|
|
||||||
|
|
||||||
when:
|
|
||||||
provider.register(className, new TypePool.Resolution.Simple(TypeDescription.VOID))
|
|
||||||
|
|
||||||
then:
|
|
||||||
provider.size() == 1
|
|
||||||
provider.find(className) == new TypePool.Resolution.Simple(TypeDescription.VOID)
|
|
||||||
|
|
||||||
when:
|
|
||||||
provider.clear()
|
|
||||||
|
|
||||||
then:
|
|
||||||
provider.size() == 0
|
|
||||||
provider.find(className) == null
|
|
||||||
|
|
||||||
where:
|
|
||||||
className = "SomeClass"
|
|
||||||
}
|
|
||||||
|
|
||||||
def "test timeout eviction"() {
|
|
||||||
setup:
|
|
||||||
def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, timeout, TimeUnit.MILLISECONDS)
|
|
||||||
def resolutionRef = new AtomicReference<TypePool.Resolution>(new TypePool.Resolution.Simple(TypeDescription.VOID))
|
|
||||||
def weakRef = new WeakReference(resolutionRef.get())
|
|
||||||
|
|
||||||
when:
|
|
||||||
def lastAccess = System.nanoTime()
|
|
||||||
provider.register(className, resolutionRef.get())
|
|
||||||
|
|
||||||
then:
|
|
||||||
// Ensure continued access prevents expiration.
|
|
||||||
for (int i = 0; i < timeout + 10; i++) {
|
|
||||||
assert TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - lastAccess) < timeout: "test took too long on " + i
|
|
||||||
assert provider.find(className) != null
|
|
||||||
assert provider.size() == 1
|
|
||||||
lastAccess = System.nanoTime()
|
|
||||||
Thread.sleep(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
when:
|
|
||||||
Thread.sleep(timeout)
|
|
||||||
|
|
||||||
then:
|
|
||||||
provider.find(className) == null
|
|
||||||
|
|
||||||
when:
|
|
||||||
provider.register(className, resolutionRef.get())
|
|
||||||
resolutionRef.set(null)
|
|
||||||
GCUtils.awaitGC(weakRef)
|
|
||||||
|
|
||||||
then:
|
|
||||||
// Verify properly GC'd
|
|
||||||
provider.find(className) == null
|
|
||||||
weakRef.get() == null
|
|
||||||
|
|
||||||
where:
|
|
||||||
className = "SomeClass"
|
|
||||||
timeout = 500 // Takes about 50 ms locally, adding an order of magnitude for CI.
|
|
||||||
}
|
|
||||||
|
|
||||||
def "test size limit"() {
|
|
||||||
setup:
|
|
||||||
def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, 2, TimeUnit.MINUTES)
|
|
||||||
def typeDef = new TypePool.Resolution.Simple(TypeDescription.VOID)
|
|
||||||
for (int i = 0; i < 10000; i++) {
|
|
||||||
provider.register("ClassName$i", typeDef)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect:
|
|
||||||
provider.size() == 5000
|
|
||||||
|
|
||||||
when:
|
|
||||||
provider.clear()
|
|
||||||
|
|
||||||
then:
|
|
||||||
provider.size() == 0
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -78,7 +78,7 @@ class ClassLoadingTest extends Specification {
|
||||||
loader.count == countAfterFirstLoad
|
loader.count == countAfterFirstLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
def "make sure that ByteBuddy doesn't resue cached type descriptions between different classloaders"() {
|
def "make sure that ByteBuddy doesn't reuse cached type descriptions between different classloaders"() {
|
||||||
setup:
|
setup:
|
||||||
CountingClassLoader loader1 = new CountingClassLoader(classpath)
|
CountingClassLoader loader1 = new CountingClassLoader(classpath)
|
||||||
CountingClassLoader loader2 = new CountingClassLoader(classpath)
|
CountingClassLoader loader2 = new CountingClassLoader(classpath)
|
||||||
|
|
Loading…
Reference in New Issue