Precompute hash code in `AspectValueKey` and `AbstractSkyKey`.
All instances of these classes are interned.
PiperOrigin-RevId: 371412973
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/AspectValueKey.java b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValueKey.java
index 63f284d..1d73b1c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/AspectValueKey.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/AspectValueKey.java
@@ -82,40 +82,39 @@
@Nullable BuildConfiguration targetConfiguration,
Label starlarkFileLabel,
String starlarkExportName) {
- StarlarkAspectLoadingKey key =
- new StarlarkAspectLoadingKey(
- targetLabel,
- aspectConfiguration == null ? null : BuildConfigurationValue.key(aspectConfiguration),
- ConfiguredTargetKey.builder()
- .setLabel(targetLabel)
- .setConfiguration(targetConfiguration)
- .build(),
- starlarkFileLabel,
- starlarkExportName);
-
- return starlarkAspectKeyInterner.intern(key);
+ return StarlarkAspectLoadingKey.createInternal(
+ targetLabel,
+ aspectConfiguration == null ? null : BuildConfigurationValue.key(aspectConfiguration),
+ ConfiguredTargetKey.builder()
+ .setLabel(targetLabel)
+ .setConfiguration(targetConfiguration)
+ .build(),
+ starlarkFileLabel,
+ starlarkExportName);
}
// Specific subtypes of aspect keys.
- /** A base class for a key representing an aspect applied to a particular target. */
+ /** Represents an aspect applied to a particular target. */
@AutoCodec
- public static class AspectKey extends AspectValueKey {
+ public static final class AspectKey extends AspectValueKey {
+ private final ConfiguredTargetKey baseConfiguredTargetKey;
private final ImmutableList<AspectKey> baseKeys;
@Nullable private final BuildConfigurationValue.Key aspectConfigurationKey;
- private final ConfiguredTargetKey baseConfiguredTargetKey;
private final AspectDescriptor aspectDescriptor;
- private int hashCode;
+ private final int hashCode;
private AspectKey(
- @Nullable BuildConfigurationValue.Key aspectConfigurationKey,
ConfiguredTargetKey baseConfiguredTargetKey,
ImmutableList<AspectKey> baseKeys,
- AspectDescriptor aspectDescriptor) {
+ AspectDescriptor aspectDescriptor,
+ @Nullable BuildConfigurationValue.Key aspectConfigurationKey,
+ int hashCode) {
this.baseKeys = baseKeys;
this.aspectConfigurationKey = aspectConfigurationKey;
this.baseConfiguredTargetKey = baseConfiguredTargetKey;
this.aspectDescriptor = aspectDescriptor;
+ this.hashCode = hashCode;
}
@AutoCodec.VisibleForSerialization
@@ -127,7 +126,12 @@
@Nullable BuildConfigurationValue.Key aspectConfigurationKey) {
return aspectKeyInterner.intern(
new AspectKey(
- aspectConfigurationKey, baseConfiguredTargetKey, baseKeys, aspectDescriptor));
+ baseConfiguredTargetKey,
+ baseKeys,
+ aspectDescriptor,
+ aspectConfigurationKey,
+ Objects.hashCode(
+ baseConfiguredTargetKey, baseKeys, aspectDescriptor, aspectConfigurationKey)));
}
@Override
@@ -169,8 +173,8 @@
return String.format("%s of %s",
aspectDescriptor.getAspectClass().getName(), getLabel());
} else {
- return String.format("%s on top of %s",
- aspectDescriptor.getAspectClass().getName(), baseKeys.toString());
+ return String.format(
+ "%s on top of %s", aspectDescriptor.getAspectClass().getName(), baseKeys);
}
}
@@ -205,33 +209,7 @@
@Override
public int hashCode() {
- // We use the hash code caching strategy employed by java.lang.String. There are three subtle
- // things going on here:
- //
- // (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet.
- // Yes, this means that if the hash code is really 0 then we will "recompute" it each time.
- // But this isn't a problem in practice since a hash code of 0 should be rare.
- //
- // (2) Since we have no synchronization, multiple threads can race here thinking there are the
- // first one to compute and cache the hash code.
- //
- // (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one
- // thread may not be visible by another.
- //
- // All three of these issues are benign from a correctness perspective; in the end we have no
- // overhead from synchronization, at the cost of potentially computing the hash code more than
- // once.
- int h = hashCode;
- if (h == 0) {
- h = computeHashCode();
- hashCode = h;
- }
- return h;
- }
-
- private int computeHashCode() {
- return Objects.hashCode(
- baseKeys, aspectConfigurationKey, baseConfiguredTargetKey, aspectDescriptor);
+ return hashCode;
}
@Override
@@ -239,13 +217,12 @@
if (this == other) {
return true;
}
-
if (!(other instanceof AspectKey)) {
return false;
}
-
AspectKey that = (AspectKey) other;
- return Objects.equal(baseKeys, that.baseKeys)
+ return hashCode == that.hashCode
+ && Objects.equal(baseKeys, that.baseKeys)
&& Objects.equal(aspectConfigurationKey, that.aspectConfigurationKey)
&& Objects.equal(baseConfiguredTargetKey, that.baseConfiguredTargetKey)
&& Objects.equal(aspectDescriptor, that.aspectDescriptor);
@@ -256,10 +233,7 @@
return "null";
}
- String baseKeysString =
- baseKeys.isEmpty()
- ? ""
- : String.format(" (over %s)", baseKeys.toString());
+ String baseKeysString = baseKeys.isEmpty() ? "" : String.format(" (over %s)", baseKeys);
return String.format(
"%s with aspect %s%s",
getLabel(), aspectDescriptor.getAspectClass().getName(), baseKeysString);
@@ -296,26 +270,51 @@
}
/** The key for a Starlark aspect. */
- public static class StarlarkAspectLoadingKey extends AspectValueKey {
-
+ @AutoCodec
+ public static final class StarlarkAspectLoadingKey extends AspectValueKey {
private final Label targetLabel;
private final BuildConfigurationValue.Key aspectConfigurationKey;
private final ConfiguredTargetKey baseConfiguredTargetKey;
private final Label starlarkFileLabel;
private final String starlarkValueName;
- private int hashCode;
+ private final int hashCode;
+
+ @AutoCodec.Instantiator
+ @AutoCodec.VisibleForSerialization
+ static StarlarkAspectLoadingKey createInternal(
+ Label targetLabel,
+ BuildConfigurationValue.Key aspectConfigurationKey,
+ ConfiguredTargetKey baseConfiguredTargetKey,
+ Label starlarkFileLabel,
+ String starlarkValueName) {
+ return starlarkAspectKeyInterner.intern(
+ new StarlarkAspectLoadingKey(
+ targetLabel,
+ aspectConfigurationKey,
+ baseConfiguredTargetKey,
+ starlarkFileLabel,
+ starlarkValueName,
+ Objects.hashCode(
+ targetLabel,
+ aspectConfigurationKey,
+ baseConfiguredTargetKey,
+ starlarkFileLabel,
+ starlarkValueName)));
+ }
private StarlarkAspectLoadingKey(
Label targetLabel,
BuildConfigurationValue.Key aspectConfigurationKey,
ConfiguredTargetKey baseConfiguredTargetKey,
Label starlarkFileLabel,
- String starlarkFunctionName) {
+ String starlarkValueName,
+ int hashCode) {
this.targetLabel = targetLabel;
this.aspectConfigurationKey = aspectConfigurationKey;
this.baseConfiguredTargetKey = baseConfiguredTargetKey;
this.starlarkFileLabel = starlarkFileLabel;
- this.starlarkValueName = starlarkFunctionName;
+ this.starlarkValueName = starlarkValueName;
+ this.hashCode = hashCode;
}
@Override
@@ -361,37 +360,7 @@
@Override
public int hashCode() {
- // We use the hash code caching strategy employed by java.lang.String. There are three subtle
- // things going on here:
- //
- // (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet.
- // Yes, this means that if the hash code is really 0 then we will "recompute" it each time.
- // But this isn't a problem in practice since a hash code of 0 should be rare.
- //
- // (2) Since we have no synchronization, multiple threads can race here thinking there are the
- // first one to compute and cache the hash code.
- //
- // (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one
- // thread may not be visible by another.
- //
- // All three of these issues are benign from a correctness perspective; in the end we have no
- // overhead from synchronization, at the cost of potentially computing the hash code more than
- // once.
- int h = hashCode;
- if (h == 0) {
- h = computeHashCode();
- hashCode = h;
- }
- return h;
- }
-
- private int computeHashCode() {
- return Objects.hashCode(
- targetLabel,
- aspectConfigurationKey,
- baseConfiguredTargetKey,
- starlarkFileLabel,
- starlarkValueName);
+ return hashCode;
}
@Override
@@ -399,12 +368,12 @@
if (o == this) {
return true;
}
-
if (!(o instanceof StarlarkAspectLoadingKey)) {
return false;
}
StarlarkAspectLoadingKey that = (StarlarkAspectLoadingKey) o;
- return Objects.equal(targetLabel, that.targetLabel)
+ return hashCode == that.hashCode
+ && Objects.equal(targetLabel, that.targetLabel)
&& Objects.equal(aspectConfigurationKey, that.aspectConfigurationKey)
&& Objects.equal(baseConfiguredTargetKey, that.baseConfiguredTargetKey)
&& Objects.equal(starlarkFileLabel, that.starlarkFileLabel)
diff --git a/src/main/java/com/google/devtools/build/skyframe/AbstractSkyKey.java b/src/main/java/com/google/devtools/build/skyframe/AbstractSkyKey.java
index 7c7fe27..22aac53 100644
--- a/src/main/java/com/google/devtools/build/skyframe/AbstractSkyKey.java
+++ b/src/main/java/com/google/devtools/build/skyframe/AbstractSkyKey.java
@@ -14,50 +14,40 @@
package com.google.devtools.build.skyframe;
-import com.google.common.base.Preconditions;
+import static com.google.common.base.Preconditions.checkNotNull;
/**
* For use when the {@link #argument} of the {@link SkyKey} cannot be a {@link SkyKey} itself,
* either because it is a type like List or because it is already a different {@link SkyKey}.
* Provides convenient boilerplate.
+ *
+ * <p>An argument's hash code might not be stable across JVM instances if it transitively depends on
+ * an object that uses the default identity-based {@link Object#hashCode} or {@link
+ * System#identityHashCode}. For this reason, the {@link #hashCode} field is {@code transient}.
+ * Subclasses should manage serialization (i.e. using {@code AutoCodec}) to ensure that {@link
+ * #AbstractSkyKey(Object)} is invoked on deserialization to freshly compute the hash code.
*/
public abstract class AbstractSkyKey<T> implements SkyKey {
+
// Visible for serialization.
protected final T arg;
+
/**
- * Cache the hash code for this object. It might be expensive to compute. It is transient because
- * argument's hash code might not be stable across JVM instances.
+ * Caches the hash code for this object. It might be expensive to compute.
+ *
+ * <p>The hash code is computed eagerly because it is expected that instances are interned,
+ * meaning that {@link #hashCode()} will be called immediately for every instance.
*/
- private transient int hashCode;
+ private final transient int hashCode;
protected AbstractSkyKey(T arg) {
- this.arg = Preconditions.checkNotNull(arg);
+ this.arg = checkNotNull(arg);
+ this.hashCode = 31 * functionName().hashCode() + arg.hashCode();
}
@Override
public final int hashCode() {
- // We use the hash code caching strategy employed by java.lang.String. There are three subtle
- // things going on here:
- //
- // (1) We use a value of 0 to indicate that the hash code hasn't been computed and cached yet.
- // Yes, this means that if the hash code is really 0 then we will "recompute" it each time. But
- // this isn't a problem in practice since a hash code of 0 should be rare.
- //
- // (2) Since we have no synchronization, multiple threads can race here thinking there are the
- // first one to compute and cache the hash code.
- //
- // (3) Moreover, since 'hashCode' is non-volatile, the cached hash code value written from one
- // thread may not be visible by another.
- //
- // All three of these issues are benign from a correctness perspective; in the end we have no
- // overhead from synchronization, at the cost of potentially computing the hash code more than
- // once.
- int h = hashCode;
- if (h == 0) {
- h = computeHashCode();
- hashCode = h;
- }
- return h;
+ return hashCode;
}
@Override
@@ -65,10 +55,6 @@
return arg;
}
- private int computeHashCode() {
- return 31 * functionName().hashCode() + arg.hashCode();
- }
-
@Override
public final boolean equals(Object obj) {
if (this == obj) {
@@ -78,7 +64,9 @@
return false;
}
AbstractSkyKey<?> that = (AbstractSkyKey<?>) obj;
- return this.functionName().equals(that.functionName()) && this.arg.equals(that.arg);
+ return hashCode == that.hashCode
+ && functionName().equals(that.functionName())
+ && arg.equals(that.arg);
}
@Override
diff --git a/src/test/java/com/google/devtools/build/skyframe/BUILD b/src/test/java/com/google/devtools/build/skyframe/BUILD
index 1aff611..9dc2576 100644
--- a/src/test/java/com/google/devtools/build/skyframe/BUILD
+++ b/src/test/java/com/google/devtools/build/skyframe/BUILD
@@ -53,6 +53,8 @@
"//src/main/java/com/google/devtools/build/lib/collect/nestedset",
"//src/main/java/com/google/devtools/build/lib/concurrent",
"//src/main/java/com/google/devtools/build/lib/events",
+ "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
+ "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils",
"//src/main/java/com/google/devtools/build/lib/util",
"//src/main/java/com/google/devtools/build/skyframe",
"//src/main/java/com/google/devtools/build/skyframe:graph_inconsistency_java_proto",
diff --git a/src/test/java/com/google/devtools/build/skyframe/SkyKeyTest.java b/src/test/java/com/google/devtools/build/skyframe/SkyKeyTest.java
index d589a7c..2a6c089 100644
--- a/src/test/java/com/google/devtools/build/skyframe/SkyKeyTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/SkyKeyTest.java
@@ -15,9 +15,11 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.collect.Interner;
-import com.google.devtools.build.lib.concurrent.BlazeInterners;
-import com.google.devtools.build.lib.testutil.TestUtils;
+import com.google.common.collect.ImmutableClassToInstanceMap;
+import com.google.devtools.build.lib.skyframe.serialization.DeserializationContext;
+import com.google.devtools.build.lib.skyframe.serialization.SerializationContext;
+import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
+import com.google.devtools.build.lib.skyframe.serialization.testutils.TestUtils;
import java.io.Serializable;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,70 +35,64 @@
public void testHashCodeTransience() throws Exception {
// Given a freshly constructed HashCodeSpy object,
HashCodeSpy hashCodeSpy = new HashCodeSpy();
- assertThat(hashCodeSpy.getNumberOfTimesHashCodeCalled()).isEqualTo(0);
+ assertThat(hashCodeSpy.numberOfTimesHashCodeCalled).isEqualTo(0);
// When a SkyKey is constructed with that HashCodeSpy as its argument,
- SkyKey originalKey = Key.create(hashCodeSpy);
+ SkyKey originalKey = new Key(hashCodeSpy);
// Then the HashCodeSpy reports that its hashcode method was called once.
- assertThat(hashCodeSpy.getNumberOfTimesHashCodeCalled()).isEqualTo(1);
-
+ assertThat(hashCodeSpy.numberOfTimesHashCodeCalled).isEqualTo(1);
// When the SkyKey's hashCode method is called,
originalKey.hashCode();
// Then the spy's hashCode method isn't called, because the SkyKey's hashCode was cached.
- assertThat(hashCodeSpy.getNumberOfTimesHashCodeCalled()).isEqualTo(1);
-
+ assertThat(hashCodeSpy.numberOfTimesHashCodeCalled).isEqualTo(1);
// When that SkyKey is serialized and then deserialized,
- SkyKey newKey = (SkyKey) TestUtils.deserializeObject(TestUtils.serializeObject(originalKey));
+ SkyKey newKey =
+ (SkyKey)
+ TestUtils.fromBytes(
+ new DeserializationContext(ImmutableClassToInstanceMap.of()),
+ TestUtils.toBytes(
+ new SerializationContext(ImmutableClassToInstanceMap.of()), originalKey));
- // Then the new SkyKey's HashCodeSpy has not had its hashCode method called.
+ // Then the new SkyKey recomputed its hashcode on deserialization.
+ assertThat(newKey.hashCode()).isEqualTo(originalKey.hashCode());
HashCodeSpy spyInNewKey = (HashCodeSpy) newKey.argument();
- assertThat(spyInNewKey.getNumberOfTimesHashCodeCalled()).isEqualTo(0);
+ assertThat(spyInNewKey.numberOfTimesHashCodeCalled).isEqualTo(1);
-
- // When the new SkyKey's hashCode method is called once,
+ // When the new SkyKey's hashCode method is called,
newKey.hashCode();
- // Then the new SkyKey's spy's hashCode method gets called.
- assertThat(spyInNewKey.getNumberOfTimesHashCodeCalled()).isEqualTo(1);
-
-
- // When the new SkyKey's hashCode method is called a second time,
- newKey.hashCode();
-
- // Then the new SkyKey's spy's hashCOde isn't called a second time, because the SkyKey's
- // hashCode was cached.
- assertThat(spyInNewKey.getNumberOfTimesHashCodeCalled()).isEqualTo(1);
+ // Then the new SkyKey's spy's hashCode method is not called again.
+ assertThat(spyInNewKey.numberOfTimesHashCodeCalled).isEqualTo(1);
}
- private static class HashCodeSpy implements Serializable {
+ static final class HashCodeSpy implements Serializable {
private transient int numberOfTimesHashCodeCalled;
- public int getNumberOfTimesHashCodeCalled() {
- return numberOfTimesHashCodeCalled;
- }
-
@Override
public int hashCode() {
numberOfTimesHashCodeCalled++;
return 42;
}
+
+ // Implemented so that numberOfTimesHashCodeCalled is not incremented when the debugger calls
+ // toString() - the default Object#toString() calls hashCode().
+ @Override
+ public String toString() {
+ return String.format("HashCodeSpy{count=%s}", numberOfTimesHashCodeCalled);
+ }
}
- private static class Key extends AbstractSkyKey<HashCodeSpy> {
- private static final Interner<Key> interner = BlazeInterners.newWeakInterner();
+ @AutoCodec
+ static final class Key extends AbstractSkyKey<HashCodeSpy> {
- private Key(HashCodeSpy arg) {
+ Key(HashCodeSpy arg) {
super(arg);
}
- private static Key create(HashCodeSpy arg) {
- return interner.intern(new Key(arg));
- }
-
@Override
public SkyFunctionName functionName() {
return SkyFunctionName.FOR_TESTING;