Skip to content

Commit d660020

Browse files
authored
Use multiple locks in ByteBuddyMockFactory to reduce lock contention (#1778)
Changed locking scheme in ByteBuddyMockFactory from a single global lock CACHE to cacheLocks with size 16. The used TypeCachingLock from cacheLocks depends on the hashcode of TypeCache.SimpleKey, which aggregates the types to mock. The old global CACHE lock did block Threads regardless, if they try to mock the same type or not. This is a similar fix as in Mockito: mockito/mockito#3095
1 parent 31985e3 commit d660020

File tree

4 files changed

+285
-8
lines changed

4 files changed

+285
-8
lines changed

spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 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+
117
package org.spockframework.mock.runtime;
218

319
import net.bytebuddy.ByteBuddy;
@@ -13,33 +29,66 @@
1329
import net.bytebuddy.implementation.MethodDelegation;
1430
import net.bytebuddy.implementation.bind.annotation.Morph;
1531
import org.jetbrains.annotations.NotNull;
32+
import org.jetbrains.annotations.VisibleForTesting;
1633
import org.spockframework.mock.ISpockMockObject;
1734
import org.spockframework.mock.codegen.Target;
1835
import org.spockframework.util.Nullable;
1936

2037
import java.lang.reflect.Method;
2138
import java.util.List;
39+
import java.util.concurrent.Callable;
2240
import java.util.concurrent.ThreadLocalRandom;
2341

2442
import static net.bytebuddy.matcher.ElementMatchers.any;
2543
import static net.bytebuddy.matcher.ElementMatchers.none;
2644

2745
class ByteBuddyMockFactory {
46+
/**
47+
* The mask to use to mask out the {@link TypeCache.SimpleKey#hashCode()} to find the {@link #cacheLocks}.
48+
*/
49+
private static final int CACHE_LOCK_MASK = 0x0F;
50+
51+
/**
52+
* The size of the {@link #cacheLocks}.
53+
*/
54+
private static final int CACHE_LOCK_SIZE = CACHE_LOCK_MASK + 1;
2855

29-
private static final TypeCache<TypeCache.SimpleKey> CACHE =
30-
new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
3156
private static final Class<?> CODEGEN_TARGET_CLASS = Target.class;
3257
private static final String CODEGEN_PACKAGE = CODEGEN_TARGET_CLASS.getPackage().getName();
3358

34-
static Object createMock(final Class<?> type,
59+
/**
60+
* This array contains {@link TypeCachingLock} instances, which are used as java monitor locks for
61+
* {@link TypeCache#findOrInsert(ClassLoader, Object, Callable, Object)}.
62+
* The {@code cacheLocks} spreads the lock to acquire over multiple locks instead of using a single lock
63+
* {@code CACHE} for all {@link TypeCache.SimpleKey}s.
64+
*
65+
* <p>Note: We can't simply use the mockedType class lock as a lock,
66+
* because the {@code TypeCache.SimpleKey}, will be the same for different {@code mockTypes + additionalInterfaces}.
67+
* See the {@code hashCode} implementation of the {@code TypeCache.SimpleKey}, which has {@code Set} semantics.
68+
*/
69+
private final TypeCachingLock[] cacheLocks;
70+
71+
private final TypeCache<TypeCache.SimpleKey> CACHE =
72+
new TypeCache.WithInlineExpunction<>(TypeCache.Sort.SOFT);
73+
74+
ByteBuddyMockFactory() {
75+
cacheLocks = new TypeCachingLock[CACHE_LOCK_SIZE];
76+
for (int i = 0; i < CACHE_LOCK_SIZE; i++) {
77+
cacheLocks[i] = new TypeCachingLock();
78+
}
79+
}
80+
81+
Object createMock(final Class<?> type,
3582
final List<Class<?>> additionalInterfaces,
3683
@Nullable List<Object> constructorArgs,
3784
IProxyBasedMockInterceptor interceptor,
3885
final ClassLoader classLoader,
3986
boolean useObjenesis) {
4087

88+
TypeCache.SimpleKey key = new TypeCache.SimpleKey(type, additionalInterfaces);
89+
4190
Class<?> enhancedType = CACHE.findOrInsert(classLoader,
42-
new TypeCache.SimpleKey(type, additionalInterfaces),
91+
key,
4392
() -> {
4493
String typeName = type.getName();
4594
Class<?> targetClass = type;
@@ -68,14 +117,28 @@ static Object createMock(final Class<?> type,
68117
.make()
69118
.load(classLoader, strategy)
70119
.getLoaded();
71-
}, CACHE);
120+
}, getCacheLockForKey(key));
72121

73122
Object proxy = MockInstantiator.instantiate(type, enhancedType, constructorArgs, useObjenesis);
74123
((ByteBuddyInterceptorAdapter.InterceptorAccess) proxy).$spock_set(interceptor);
75124
return proxy;
76125
}
77126

78-
// This methods and the ones it calls are inspired by similar code in Mockito's SubclassBytecodeGenerator
127+
/**
128+
* Returns a {@link TypeCachingLock}, which locks the {@link TypeCache#findOrInsert(ClassLoader, Object, Callable, Object)}.
129+
*
130+
* @param key the key to lock
131+
* @return the {@link TypeCachingLock} to use to lock the {@link TypeCache}
132+
*/
133+
private TypeCachingLock getCacheLockForKey(TypeCache.SimpleKey key) {
134+
int hashCode = key.hashCode();
135+
// Try to spread some higher bits with XOR to lower bits, because we only use lower bits.
136+
hashCode = hashCode ^ (hashCode >>> 16);
137+
int index = hashCode & CACHE_LOCK_MASK;
138+
return cacheLocks[index];
139+
}
140+
141+
// This method and the ones it calls are inspired by similar code in Mockito's SubclassBytecodeGenerator
79142
private static boolean shouldLoadIntoCodegenPackage(Class<?> type) {
80143
return isComingFromJDK(type) || isComingFromSignedJar(type) || isComingFromSealedPackage(type);
81144
}
@@ -112,4 +175,6 @@ private static ClassLoadingStrategy<ClassLoader> determineBestClassLoadingStrate
112175
return ClassLoadingStrategy.Default.WRAPPER;
113176
}
114177

178+
private static final class TypeCachingLock {
179+
}
115180
}

spock-core/src/main/java/org/spockframework/mock/runtime/ProxyBasedMockFactory.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,24 @@ public class ProxyBasedMockFactory {
3333

3434
public static ProxyBasedMockFactory INSTANCE = new ProxyBasedMockFactory();
3535

36+
@Nullable
37+
private final ByteBuddyMockFactory byteBuddyMockFactory;
38+
39+
private ProxyBasedMockFactory() {
40+
if (byteBuddyAvailable && !ignoreByteBuddy) {
41+
byteBuddyMockFactory = new ByteBuddyMockFactory();
42+
} else {
43+
byteBuddyMockFactory = null;
44+
}
45+
}
46+
3647
public Object create(Class<?> mockType, List<Class<?>> additionalInterfaces, @Nullable List<Object> constructorArgs,
3748
IProxyBasedMockInterceptor mockInterceptor, ClassLoader classLoader, boolean useObjenesis) throws CannotCreateMockException {
3849
if (mockType.isInterface()) {
3950
return DynamicProxyMockFactory.createMock(mockType, additionalInterfaces, constructorArgs, mockInterceptor, classLoader);
4051
}
41-
if (byteBuddyAvailable && !ignoreByteBuddy) {
42-
return ByteBuddyMockFactory.createMock(mockType, additionalInterfaces, constructorArgs, mockInterceptor, classLoader, useObjenesis);
52+
if (byteBuddyMockFactory != null) {
53+
return byteBuddyMockFactory.createMock(mockType, additionalInterfaces, constructorArgs, mockInterceptor, classLoader, useObjenesis);
4354
}
4455
if (cglibAvailable) {
4556
return CglibMockFactory.createMock(mockType, additionalInterfaces, constructorArgs, mockInterceptor, classLoader, useObjenesis);
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 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.spockframework.mock.runtime
18+
19+
import groovy.transform.Canonical
20+
import spock.lang.Shared
21+
import spock.lang.Specification
22+
import spock.lang.Timeout
23+
24+
import java.util.concurrent.CompletableFuture
25+
import java.util.concurrent.Phaser
26+
import java.util.concurrent.TimeUnit
27+
import java.util.function.Function
28+
29+
class ByteBuddyMockFactoryConcurrentSpec extends Specification {
30+
private static final String IfA = "IfA"
31+
private static final String IfB = "IfB"
32+
private static final String IfC = "IfC"
33+
@Shared
34+
final IProxyBasedMockInterceptor interceptor = Stub()
35+
36+
def "ensure lockMask bit patterns"() {
37+
expect:
38+
1 << Integer.bitCount(ByteBuddyMockFactory.CACHE_LOCK_MASK) - 1 == Integer.highestOneBit(ByteBuddyMockFactory.CACHE_LOCK_MASK)
39+
}
40+
41+
// Just to be save to abort, normally the tests run in 2 secs.
42+
@Timeout(40)
43+
def "cacheLockingStressTest #test"() {
44+
given:
45+
int iterations = 5_000
46+
def tempClassLoader = new ByteBuddyTestClassLoader()
47+
MockFeatures featA = toMockFeatures(mockSpecA, tempClassLoader)
48+
MockFeatures featB = toMockFeatures(mockSpecB, tempClassLoader)
49+
ByteBuddyMockFactory mockFactory = new ByteBuddyMockFactory()
50+
51+
Phaser phaser = new Phaser(4)
52+
Function<Runnable, CompletableFuture<Void>> runCode = { Runnable code ->
53+
CompletableFuture.runAsync {
54+
phaser.arriveAndAwaitAdvance()
55+
try {
56+
for (int i = 0; i < iterations; i++) {
57+
code.run()
58+
}
59+
} finally {
60+
phaser.arrive()
61+
}
62+
}
63+
}
64+
when:
65+
def mockFeatAFuture = runCode.apply {
66+
Class<?> mockClass = mockClass(mockFactory, tempClassLoader, featA)
67+
assertValidMockClass(featA, mockClass, tempClassLoader)
68+
}
69+
70+
def mockFeatBFuture = runCode.apply {
71+
Class<?> mockClass = mockClass(mockFactory, tempClassLoader, featB)
72+
assertValidMockClass(featB, mockClass, tempClassLoader)
73+
}
74+
75+
def cacheFuture = runCode.apply { mockFactory.CACHE.clear() }
76+
77+
phaser.arriveAndAwaitAdvance()
78+
// Wait for test to end
79+
int phase = phaser.arrive()
80+
try {
81+
phaser.awaitAdvanceInterruptibly(phase, 30, TimeUnit.SECONDS)
82+
} finally {
83+
// Collect exceptions from the futures, to make issues visible.
84+
mockFeatAFuture.getNow(null)
85+
mockFeatBFuture.getNow(null)
86+
cacheFuture.getNow(null)
87+
}
88+
then:
89+
noExceptionThrown()
90+
91+
where:
92+
test | mockSpecA | mockSpecB
93+
"same hashcode different mockType" | mockSpec(IfA, IfB) | mockSpec(IfB, IfA)
94+
"same hashcode same mockType" | mockSpec(IfA) | mockSpec(IfA)
95+
"different hashcode different interfaces" | mockSpec(IfA, IfB) | mockSpec(IfB, IfC)
96+
"unrelated classes" | mockSpec(IfA) | mockSpec(IfB)
97+
}
98+
99+
private Class<?> mockClass(ByteBuddyMockFactory mockFactory, ClassLoader cl, MockFeatures feature) {
100+
return mockFactory.createMock(
101+
feature.mockType,
102+
feature.interfaces,
103+
null,
104+
interceptor,
105+
cl,
106+
false)
107+
.getClass()
108+
}
109+
110+
private static MockSpec mockSpec(String mockedType, String... interfaces) {
111+
return new MockSpec(mockedType, interfaces as List)
112+
}
113+
114+
private void assertValidMockClass(MockFeatures mockFeature, Class<?> mockClass, ClassLoader classLoader) {
115+
assert mockClass.classLoader == classLoader
116+
assert mockFeature.mockType.isAssignableFrom(mockClass)
117+
mockFeature.interfaces.each {
118+
assert it.isAssignableFrom(mockClass)
119+
}
120+
}
121+
122+
MockFeatures toMockFeatures(MockSpec mockFeaturesString, ByteBuddyTestClassLoader classLoader) {
123+
def mockType = classLoader.defineInterface(mockFeaturesString.mockType)
124+
def interfaces = mockFeaturesString.interfaces.collect { classLoader.defineInterface(it) }
125+
return new MockFeatures(mockType, interfaces)
126+
}
127+
128+
/**
129+
* Class holding the loaded classes to mock.
130+
*/
131+
@Canonical
132+
private static class MockFeatures {
133+
final Class<?> mockType
134+
final List<Class<?>> interfaces
135+
}
136+
137+
/**
138+
* Class holding the class names to mock.
139+
* Which will be converted into a {@link MockFeatures} during test.
140+
*/
141+
@Canonical
142+
private static class MockSpec {
143+
final String mockType
144+
final List<String> interfaces
145+
}
146+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 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.spockframework.mock.runtime;
18+
19+
import net.bytebuddy.ByteBuddy;
20+
import net.bytebuddy.description.type.TypeDescription;
21+
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
22+
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
final class ByteBuddyTestClassLoader extends ClassLoader {
27+
private final Map<String, Class<?>> cache = new HashMap<>();
28+
29+
private final ClassLoadingStrategy<ByteBuddyTestClassLoader> loadingStrategy = (loader, types) -> {
30+
Map<TypeDescription, Class<?>> result = new HashMap<>();
31+
for (Map.Entry<TypeDescription, byte[]> entry : types.entrySet()) {
32+
TypeDescription description = entry.getKey();
33+
byte[] bytes = entry.getValue();
34+
Class<?> clazz = defineClass(description.getName(), bytes, 0, bytes.length);
35+
result.put(description, clazz);
36+
}
37+
return result;
38+
};
39+
40+
/**
41+
* Defines an empty interface with the passed {@code node}.
42+
*
43+
* @param name the name of the interface
44+
* @return the loaded {@code Class}
45+
*/
46+
synchronized Class<?> defineInterface(String name) {
47+
//noinspection resource
48+
return cache.computeIfAbsent(name, nameKey -> new ByteBuddy()
49+
.makeInterface()
50+
.name(nameKey)
51+
.make()
52+
.load(this, loadingStrategy)
53+
.getLoaded());
54+
}
55+
}

0 commit comments

Comments
 (0)