Skip to content

Commit 33deaff

Browse files
committed
Move performance test to JMH benchmark
This commit removes an ignored performance test in the `ConcurrentReferenceHashMap` test suite and converts it to a JMH benchmark.
1 parent 1fb2a37 commit 33deaff

File tree

2 files changed

+118
-58
lines changed

2 files changed

+118
-58
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.util;
18+
19+
20+
import java.lang.ref.WeakReference;
21+
import java.util.ArrayList;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Random;
26+
import java.util.WeakHashMap;
27+
import java.util.function.Function;
28+
29+
import org.openjdk.jmh.annotations.Benchmark;
30+
import org.openjdk.jmh.annotations.BenchmarkMode;
31+
import org.openjdk.jmh.annotations.Level;
32+
import org.openjdk.jmh.annotations.Mode;
33+
import org.openjdk.jmh.annotations.Param;
34+
import org.openjdk.jmh.annotations.Scope;
35+
import org.openjdk.jmh.annotations.Setup;
36+
import org.openjdk.jmh.annotations.State;
37+
import org.openjdk.jmh.infra.Blackhole;
38+
39+
/**
40+
* Benchmarks for {@link ConcurrentReferenceHashMap}.
41+
* <p>This benchmark ensures that {@link ConcurrentReferenceHashMap} performs
42+
* better than {@link java.util.Collections#synchronizedMap(Map)} with
43+
* concurrent read operations.
44+
* <p>Typically this can be run with {@code "java -jar spring-core-jmh.jar -t 30 -f 2 ConcurrentReferenceHashMapBenchmark"}.
45+
* @author Brian Clozel
46+
*/
47+
@BenchmarkMode(Mode.Throughput)
48+
public class ConcurrentReferenceHashMapBenchmark {
49+
50+
@Benchmark
51+
public void concurrentMap(ConcurrentMapBenchmarkData data, Blackhole bh) {
52+
for (String element : data.elements) {
53+
WeakReference<String> value = data.map.get(element);
54+
bh.consume(value);
55+
}
56+
}
57+
58+
@State(Scope.Benchmark)
59+
public static class ConcurrentMapBenchmarkData {
60+
61+
@Param({"500"})
62+
public int capacity;
63+
private final Function<String, String> generator = key -> key + "value";
64+
65+
public List<String> elements;
66+
67+
public Map<String, WeakReference<String>> map;
68+
69+
@Setup(Level.Iteration)
70+
public void setup() {
71+
this.elements = new ArrayList<>(this.capacity);
72+
this.map = new ConcurrentReferenceHashMap<>();
73+
Random random = new Random();
74+
random.ints(this.capacity).forEach(value -> {
75+
String element = String.valueOf(value);
76+
this.elements.add(element);
77+
this.map.put(element, new WeakReference<>(this.generator.apply(element)));
78+
});
79+
this.elements.sort(String::compareTo);
80+
}
81+
}
82+
83+
@Benchmark
84+
public void synchronizedMap(SynchronizedMapBenchmarkData data, Blackhole bh) {
85+
for (String element : data.elements) {
86+
WeakReference<String> value = data.map.get(element);
87+
bh.consume(value);
88+
}
89+
}
90+
91+
@State(Scope.Benchmark)
92+
public static class SynchronizedMapBenchmarkData {
93+
94+
@Param({"500"})
95+
public int capacity;
96+
97+
private Function<String, String> generator = key -> key + "value";
98+
99+
public List<String> elements;
100+
101+
public Map<String, WeakReference<String>> map;
102+
103+
104+
@Setup(Level.Iteration)
105+
public void setup() {
106+
this.elements = new ArrayList<>(this.capacity);
107+
this.map = Collections.synchronizedMap(new WeakHashMap<>());
108+
Random random = new Random();
109+
random.ints(this.capacity).forEach(value -> {
110+
String element = String.valueOf(value);
111+
this.elements.add(element);
112+
this.map.put(element, new WeakReference<>(this.generator.apply(element)));
113+
});
114+
this.elements.sort(String::compareTo);
115+
}
116+
}
117+
118+
}

spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616

1717
package org.springframework.util;
1818

19-
import java.lang.ref.WeakReference;
2019
import java.util.ArrayList;
21-
import java.util.Collections;
2220
import java.util.Comparator;
2321
import java.util.HashMap;
2422
import java.util.HashSet;
@@ -27,9 +25,7 @@
2725
import java.util.List;
2826
import java.util.Map;
2927
import java.util.Set;
30-
import java.util.WeakHashMap;
3128

32-
import org.junit.jupiter.api.Disabled;
3329
import org.junit.jupiter.api.Test;
3430

3531
import org.springframework.lang.Nullable;
@@ -507,66 +503,12 @@ void containsViaEntrySet() {
507503
copy.forEach(entry -> assertThat(entrySet.contains(entry)).isFalse());
508504
}
509505

510-
@Test
511-
@Disabled("Intended for use during development only")
512-
void shouldBeFasterThanSynchronizedMap() throws InterruptedException {
513-
Map<Integer, WeakReference<String>> synchronizedMap = Collections.synchronizedMap(new WeakHashMap<Integer, WeakReference<String>>());
514-
StopWatch mapTime = timeMultiThreaded("SynchronizedMap", synchronizedMap, v -> new WeakReference<>(String.valueOf(v)));
515-
System.out.println(mapTime.prettyPrint());
516-
517-
this.map.setDisableTestHooks(true);
518-
StopWatch cacheTime = timeMultiThreaded("WeakConcurrentCache", this.map, String::valueOf);
519-
System.out.println(cacheTime.prettyPrint());
520-
521-
// We should be at least 4 time faster
522-
assertThat(cacheTime.getTotalTimeSeconds()).isLessThan(mapTime.getTotalTimeSeconds() / 4.0);
523-
}
524-
525506
@Test
526507
void shouldSupportNullReference() {
527508
// GC could happen during restructure so we must be able to create a reference for a null entry
528509
map.createReferenceManager().createReference(null, 1234, null);
529510
}
530511

531-
/**
532-
* Time a multi-threaded access to a cache.
533-
* @return the timing stopwatch
534-
*/
535-
private <V> StopWatch timeMultiThreaded(String id, final Map<Integer, V> map,
536-
ValueFactory<V> factory) throws InterruptedException {
537-
538-
StopWatch stopWatch = new StopWatch(id);
539-
for (int i = 0; i < 500; i++) {
540-
map.put(i, factory.newValue(i));
541-
}
542-
Thread[] threads = new Thread[30];
543-
stopWatch.start("Running threads");
544-
for (int threadIndex = 0; threadIndex < threads.length; threadIndex++) {
545-
threads[threadIndex] = new Thread("Cache access thread " + threadIndex) {
546-
@Override
547-
public void run() {
548-
for (int j = 0; j < 1000; j++) {
549-
for (int i = 0; i < 1000; i++) {
550-
map.get(i);
551-
}
552-
}
553-
}
554-
};
555-
}
556-
for (Thread thread : threads) {
557-
thread.start();
558-
}
559-
560-
for (Thread thread : threads) {
561-
if (thread.isAlive()) {
562-
thread.join(2000);
563-
}
564-
}
565-
stopWatch.stop();
566-
return stopWatch;
567-
}
568-
569-
570512
private interface ValueFactory<V> {
571513

572514
V newValue(int k);

0 commit comments

Comments
 (0)