Skip to content

Commit a60d23a

Browse files
Add class cache (#3262)
Add several class cache implementations along with an integration test that demonstrates cache performance under various levels of concurrency. Implementations include a synchronized LRU map, Guava and Caffeine. --------- Co-authored-by: jeff <[email protected]>
1 parent 406e9ad commit a60d23a

File tree

6 files changed

+467
-0
lines changed

6 files changed

+467
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package datawave.core.cache;
2+
3+
import java.util.concurrent.CompletionException;
4+
import java.util.concurrent.TimeUnit;
5+
6+
import com.github.benmanes.caffeine.cache.Caffeine;
7+
import com.github.benmanes.caffeine.cache.LoadingCache;
8+
9+
/**
10+
* A {@link Caffeine} implementation of a {@link ClassCache}
11+
*/
12+
public class CaffeineClassCache implements ClassCache {
13+
14+
private final LoadingCache<String,Class<?>> cache;
15+
16+
public CaffeineClassCache() {
17+
this(DEFAULT_SIZE);
18+
}
19+
20+
public CaffeineClassCache(int size) {
21+
// @formatter:off
22+
cache = Caffeine.newBuilder()
23+
.maximumSize(size)
24+
.expireAfterAccess(1, TimeUnit.HOURS)
25+
.build(Class::forName);
26+
// @formatter:on
27+
}
28+
29+
@Override
30+
public String name() {
31+
return "Caffeine";
32+
}
33+
34+
@Override
35+
public Class<?> get(String name) throws ClassNotFoundException {
36+
try {
37+
return cache.get(name);
38+
} catch (CompletionException e) {
39+
throw new ClassNotFoundException(name, e);
40+
}
41+
}
42+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package datawave.core.cache;
2+
3+
/**
4+
* An interface that defines a {@link Class} cache.
5+
* <p>
6+
* All caches must support threadsafe operations.
7+
* <p>
8+
* No implementation should rely on static variables.
9+
*/
10+
public interface ClassCache {
11+
12+
/**
13+
* The default cache size
14+
*/
15+
int DEFAULT_SIZE = 128;
16+
17+
/**
18+
* The name of the cache implementation
19+
*
20+
* @return the name
21+
*/
22+
String name();
23+
24+
/**
25+
* Get or create a Class for the provided name
26+
*
27+
* @param name
28+
* the class name
29+
* @return a {@link Class}
30+
*/
31+
Class<?> get(String name) throws ClassNotFoundException;
32+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package datawave.core.cache;
2+
3+
import java.util.concurrent.ExecutionException;
4+
import java.util.concurrent.TimeUnit;
5+
6+
import com.google.common.cache.CacheBuilder;
7+
import com.google.common.cache.CacheLoader;
8+
import com.google.common.cache.LoadingCache;
9+
10+
/**
11+
* A guava {@link LoadingCache} implementation of a {@link ClassCache}
12+
*/
13+
public class GuavaClassCache implements ClassCache {
14+
15+
private final LoadingCache<String,Class<?>> cache;
16+
17+
public GuavaClassCache() {
18+
this(DEFAULT_SIZE);
19+
}
20+
21+
public GuavaClassCache(int size) {
22+
// @formatter:off
23+
cache = CacheBuilder.newBuilder()
24+
.maximumSize(size)
25+
.expireAfterWrite(1, TimeUnit.HOURS)
26+
.build(new CacheLoader<>() {
27+
@Override
28+
public Class<?> load(String name)throws ClassNotFoundException {
29+
return Class.forName(name);
30+
}
31+
});
32+
// @formatter:on
33+
}
34+
35+
@Override
36+
public String name() {
37+
return "Guava";
38+
}
39+
40+
@Override
41+
public Class<?> get(String name) throws ClassNotFoundException {
42+
try {
43+
return cache.get(name);
44+
} catch (ExecutionException e) {
45+
throw new ClassNotFoundException("Invalid Class name", e);
46+
}
47+
}
48+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package datawave.core.cache;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
6+
import org.apache.commons.collections4.map.LRUMap;
7+
8+
/**
9+
* A {@link LRUMap} implementation of a {@link ClassCache}
10+
*/
11+
public class LRUMapClassCache implements ClassCache {
12+
13+
private final Map<String,Class<?>> cache;
14+
15+
public LRUMapClassCache() {
16+
this(DEFAULT_SIZE);
17+
}
18+
19+
public LRUMapClassCache(int size) {
20+
cache = Collections.synchronizedMap(new LRUMap<>(size));
21+
}
22+
23+
@Override
24+
public String name() {
25+
return "LRUMap";
26+
}
27+
28+
@Override
29+
public Class<?> get(String name) throws ClassNotFoundException {
30+
Class<?> clazz = cache.get(name);
31+
if (clazz == null) {
32+
clazz = Class.forName(name);
33+
cache.put(name, clazz);
34+
}
35+
return clazz;
36+
}
37+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package datawave.core.cache;
2+
3+
import java.util.ArrayDeque;
4+
import java.util.ArrayList;
5+
import java.util.Collections;
6+
import java.util.Deque;
7+
import java.util.LinkedList;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Queue;
11+
import java.util.concurrent.Callable;
12+
import java.util.concurrent.ExecutionException;
13+
import java.util.concurrent.ExecutorService;
14+
import java.util.concurrent.Executors;
15+
import java.util.concurrent.Future;
16+
import java.util.concurrent.TimeUnit;
17+
18+
import org.apache.commons.lang3.tuple.Pair;
19+
import org.junit.jupiter.api.MethodOrderer;
20+
import org.junit.jupiter.api.Order;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.TestMethodOrder;
23+
24+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
25+
public class ClassCacheIT {
26+
27+
private final boolean debug = false;
28+
private final int maxIterations = 1_000;
29+
private final int lowThreads = 5;
30+
private final int highThreads = 50;
31+
32+
@Test
33+
@Order(1)
34+
public void testLowThreadsLowVariance() {
35+
testThreads(lowThreads, lowVariance());
36+
}
37+
38+
@Test
39+
@Order(2)
40+
public void testLowThreadsHighVariance() {
41+
testThreads(lowThreads, highVariance());
42+
}
43+
44+
@Test
45+
@Order(3)
46+
public void testHighThreadsLowVariance() {
47+
testThreads(highThreads, lowVariance());
48+
}
49+
50+
@Test
51+
@Order(4)
52+
public void testHighThreadsHighVariance() {
53+
testThreads(highThreads, highVariance());
54+
}
55+
56+
private void testThreads(int threads, List<String> names) {
57+
List<ClassCache> caches = getCaches();
58+
List<Pair<Long,String>> outputs = new ArrayList<>();
59+
for (ClassCache cache : caches) {
60+
Pair<Long,String> output = testThreads(threads, cache, names);
61+
outputs.add(output);
62+
}
63+
64+
if (debug) {
65+
Collections.sort(outputs);
66+
for (Pair<Long,String> output : outputs) {
67+
System.out.println(output.getRight());
68+
}
69+
}
70+
}
71+
72+
private Pair<Long,String> testThreads(int threads, ClassCache cache, List<String> names) {
73+
ExecutorService executor = null;
74+
try {
75+
executor = Executors.newFixedThreadPool(threads);
76+
List<Callable<Long>> callables = createCallables(threads, names, cache);
77+
List<Future<Long>> futures = executor.invokeAll(callables);
78+
79+
long total = 0L;
80+
for (Future<Long> future : futures) {
81+
long time = future.get();
82+
total += time;
83+
}
84+
85+
long time = total / ((long) threads * names.size());
86+
time = TimeUnit.NANOSECONDS.toMicros(time);
87+
88+
String line = "threads: " + threads + " size: " + names.size() + " iterations: " + maxIterations + " took " + time + " micros for " + cache.name();
89+
return Pair.of(time, line);
90+
} catch (InterruptedException | ExecutionException e) {
91+
throw new RuntimeException(e);
92+
} finally {
93+
if (executor != null) {
94+
executor.shutdownNow();
95+
}
96+
}
97+
}
98+
99+
private List<Callable<Long>> createCallables(int threads, List<String> names, ClassCache cache) {
100+
List<Callable<Long>> callables = new ArrayList<>();
101+
for (int i = 0; i < threads; i++) {
102+
final List<String> copy = new ArrayList<>(names);
103+
Collections.shuffle(copy);
104+
callables.add(() -> {
105+
long total = 0L;
106+
for (int k = 0; k < maxIterations; k++) {
107+
for (String name : copy) {
108+
long elapsed = System.nanoTime();
109+
Class<?> clazz = cache.get(name);
110+
total += System.nanoTime() - elapsed;
111+
}
112+
}
113+
return total;
114+
});
115+
}
116+
return callables;
117+
}
118+
119+
private List<ClassCache> getCaches() {
120+
List<ClassCache> caches = new ArrayList<>();
121+
caches.add(new CaffeineClassCache());
122+
caches.add(new GuavaClassCache());
123+
caches.add(new LRUMapClassCache());
124+
return caches;
125+
}
126+
127+
private List<String> lowVariance() {
128+
List<String> names = new ArrayList<>();
129+
names.add(String.class.getTypeName());
130+
names.add(Integer.class.getTypeName());
131+
names.add(Long.class.getTypeName());
132+
names.add(Double.class.getTypeName());
133+
names.add(Float.class.getTypeName());
134+
return names;
135+
}
136+
137+
private List<String> highVariance() {
138+
List<String> names = lowVariance();
139+
names.add(Map.class.getTypeName());
140+
names.add(List.class.getTypeName());
141+
names.add(LinkedList.class.getTypeName());
142+
names.add(Queue.class.getTypeName());
143+
names.add(Deque.class.getTypeName());
144+
names.add(ArrayDeque.class.getTypeName());
145+
return names;
146+
}
147+
}

0 commit comments

Comments
 (0)