Starlark: better reflection cache in CallUtils
Replace Guava cache with ConcurrentHashMap. Guava cache is quite
expensive for quick operations.
Dummy native call heavy benchmark:
```
def test():
for i in range(10):
print(i)
for j in range(1000):
for k in range(200):
list()
list()
list()
list()
list()
list()
list()
list()
list()
list()
list()
list()
test()
```
```
$ time bazel-bin/src/main/java/net/starlark/java/cmd/Starlark ./c.star
```
Before: 3.5s
After: 2.9s
Closes #11681.
PiperOrigin-RevId: 320937877
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java b/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
index 1f2db69..7ebda73 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/CallUtils.java
@@ -14,9 +14,6 @@
package com.google.devtools.build.lib.syntax;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.lang.reflect.Method;
@@ -24,7 +21,7 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
import net.starlark.java.annot.StarlarkInterfaceUtils;
import net.starlark.java.annot.StarlarkMethod;
@@ -41,11 +38,15 @@
if (cls == String.class) {
cls = StringModule.class;
}
- try {
- return cache.get(new Key(cls, semantics));
- } catch (ExecutionException ex) {
- throw new IllegalStateException("cache error", ex);
+ Key key = new Key(cls, semantics);
+
+ // Speculatively call `get` because `computeIfAbsent` is more expensive
+ // even when the map already contains a value (common case).
+ CacheValue cacheValue = cache.get(key);
+ if (cacheValue == null) {
+ cacheValue = cache.computeIfAbsent(key, CallUtils::buildCacheValue);
}
+ return cacheValue;
}
// Key is a simple Pair<Class, StarlarkSemantics>.
@@ -81,72 +82,65 @@
}
// A cache of information derived from a StarlarkMethod-annotated class and a StarlarkSemantics.
- private static final LoadingCache<Key, CacheValue> cache =
- CacheBuilder.newBuilder()
- .build(
- new CacheLoader<Key, CacheValue>() {
- @Override
- public CacheValue load(Key key) throws Exception {
- MethodDescriptor selfCall = null;
- ImmutableMap.Builder<String, MethodDescriptor> methods = ImmutableMap.builder();
- Map<String, MethodDescriptor> fields = new HashMap<>();
+ private static final ConcurrentHashMap<Key, CacheValue> cache = new ConcurrentHashMap<>();
- // Sort methods by Java name, for determinism.
- Method[] classMethods = key.cls.getMethods();
- Arrays.sort(classMethods, Comparator.comparing(Method::getName));
- for (Method method : classMethods) {
- // Synthetic methods lead to false multiple matches
- if (method.isSynthetic()) {
- continue;
- }
+ private static CacheValue buildCacheValue(Key key) {
+ MethodDescriptor selfCall = null;
+ ImmutableMap.Builder<String, MethodDescriptor> methods = ImmutableMap.builder();
+ Map<String, MethodDescriptor> fields = new HashMap<>();
- // annotated?
- StarlarkMethod callable = StarlarkInterfaceUtils.getStarlarkMethod(method);
- if (callable == null) {
- continue;
- }
+ // Sort methods by Java name, for determinism.
+ Method[] classMethods = key.cls.getMethods();
+ Arrays.sort(classMethods, Comparator.comparing(Method::getName));
+ for (Method method : classMethods) {
+ // Synthetic methods lead to false multiple matches
+ if (method.isSynthetic()) {
+ continue;
+ }
- // enabled by semantics?
- if (!key.semantics.isFeatureEnabledBasedOnTogglingFlags(
- callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
- continue;
- }
+ // annotated?
+ StarlarkMethod callable = StarlarkInterfaceUtils.getStarlarkMethod(method);
+ if (callable == null) {
+ continue;
+ }
- MethodDescriptor descriptor =
- MethodDescriptor.of(method, callable, key.semantics);
+ // enabled by semantics?
+ if (!key.semantics.isFeatureEnabledBasedOnTogglingFlags(
+ callable.enableOnlyWithFlag(), callable.disableWithFlag())) {
+ continue;
+ }
- // self-call method?
- if (callable.selfCall()) {
- if (selfCall != null) {
- throw new IllegalArgumentException(
- String.format(
- "Class %s has two selfCall methods defined", key.cls.getName()));
- }
- selfCall = descriptor;
- continue;
- }
+ MethodDescriptor descriptor = MethodDescriptor.of(method, callable, key.semantics);
- // regular method
- methods.put(callable.name(), descriptor);
+ // self-call method?
+ if (callable.selfCall()) {
+ if (selfCall != null) {
+ throw new IllegalArgumentException(
+ String.format("Class %s has two selfCall methods defined", key.cls.getName()));
+ }
+ selfCall = descriptor;
+ continue;
+ }
- // field method?
- if (descriptor.isStructField()
- && fields.put(callable.name(), descriptor) != null) {
- // TODO(b/72113542): Validate with annotation processor instead of at runtime.
- throw new IllegalArgumentException(
- String.format(
- "Class %s declares two structField methods named %s",
- key.cls.getName(), callable.name()));
- }
- }
+ // regular method
+ methods.put(callable.name(), descriptor);
- CacheValue value = new CacheValue();
- value.selfCall = selfCall;
- value.methods = methods.build();
- value.fields = ImmutableMap.copyOf(fields);
- return value;
- }
- });
+ // field method?
+ if (descriptor.isStructField() && fields.put(callable.name(), descriptor) != null) {
+ // TODO(b/72113542): Validate with annotation processor instead of at runtime.
+ throw new IllegalArgumentException(
+ String.format(
+ "Class %s declares two structField methods named %s",
+ key.cls.getName(), callable.name()));
+ }
+ }
+
+ CacheValue value = new CacheValue();
+ value.selfCall = selfCall;
+ value.methods = methods.build();
+ value.fields = ImmutableMap.copyOf(fields);
+ return value;
+ }
/**
* Returns a map of methods and corresponding StarlarkMethod annotations of the methods of the