blob: ec7a4c7e2a67cb3feded4c485d209654f2afca30 [file] [log] [blame]
// Copyright 2024 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.skyframe.serialization;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableClassToInstanceMap;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.devtools.build.lib.skyframe.serialization.DeferredObjectCodec.DeferredValue;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* {@link DeserializationContext} that performs memoization, see {@link
* MemoizingSerializationContext} for the protocol description.
*/
abstract class MemoizingDeserializationContext extends DeserializationContext {
/**
* A placeholder that keeps the size of {@link #memoTable} consistent with the numbering of its
* contents.
*/
private static final PlaceholderValue INITIAL_VALUE_PLACEHOLDER = new PlaceholderValue();
private final Int2ObjectOpenHashMap<Object> memoTable = new Int2ObjectOpenHashMap<>();
private int tagForMemoizedBefore = -1;
private final Deque<Object> memoizedBeforeStackForSanityChecking = new ArrayDeque<>();
@VisibleForTesting // private
static MemoizingDeserializationContext createForTesting(
ObjectCodecRegistry registry, ImmutableClassToInstanceMap<Object> dependencies) {
return new MemoizingDeserializationContextImpl(registry, dependencies);
}
MemoizingDeserializationContext(
ObjectCodecRegistry registry, ImmutableClassToInstanceMap<Object> dependencies) {
super(registry, dependencies);
}
static Object deserializeMemoized(
ObjectCodecRegistry codecRegistry,
ImmutableClassToInstanceMap<Object> dependencies,
ByteString bytes)
throws SerializationException {
return ObjectCodecs.deserializeStreamFully(
bytes.newCodedInput(),
new MemoizingDeserializationContextImpl(codecRegistry, dependencies));
}
static Object deserializeMemoized(
ObjectCodecRegistry codecRegistry,
ImmutableClassToInstanceMap<Object> dependencies,
byte[] bytes)
throws SerializationException {
return ObjectCodecs.deserializeStreamFully(
CodedInputStream.newInstance(bytes),
new MemoizingDeserializationContextImpl(codecRegistry, dependencies));
}
@Override
public final <T> T deserializeLeaf(CodedInputStream codedIn, LeafObjectCodec<T> codec)
throws IOException, SerializationException {
int tag = codedIn.readSInt32();
if (tag == 0) {
return null;
}
Object maybeConstant = maybeGetConstantByTag(tag);
if (maybeConstant != null) {
return codec.safeCast(maybeConstant);
}
if (tag < -1) {
// Subtracts 2 to undo the corresponding operation in SerializationContext.serializeLeaf.
return codec.safeCast(getMemoizedBackReference(-tag - 2));
}
checkState(tag == -1, "Unexpected tag for immediate value; %s", tag);
T value = codec.deserialize((LeafDeserializationContext) this, codedIn);
memoize(memoTable.size(), value);
return value;
}
@Override
public final void registerInitialValue(Object initialValue) {
checkState(tagForMemoizedBefore != -1, "Not called with memoize before: %s", initialValue);
int tag = tagForMemoizedBefore;
tagForMemoizedBefore = -1;
// Replaces the INITIAL_VALUE_PLACEHOLDER with the actual initial value.
checkState(memoTable.put(tag, initialValue) == INITIAL_VALUE_PLACEHOLDER);
memoizedBeforeStackForSanityChecking.addLast(initialValue);
}
@Override
final Object getMemoizedBackReference(int memoIndex) {
Object value = checkNotNull(memoTable.get(memoIndex), memoIndex);
checkState(
value != INITIAL_VALUE_PLACEHOLDER,
"Backreference prior to registerInitialValue: %s",
memoIndex);
return value;
}
@Override
final Object deserializeAndMaybeMemoize(ObjectCodec<?> codec, CodedInputStream codedIn)
throws SerializationException, IOException {
checkState(
tagForMemoizedBefore == -1,
"non-null memoized-before tag %s (%s)",
tagForMemoizedBefore,
codec);
switch (codec.getStrategy()) {
case MEMOIZE_BEFORE:
return deserializeMemoBeforeContent(codec, codedIn);
case MEMOIZE_AFTER:
return deserializeMemoAfterContent(codec, codedIn);
}
throw new AssertionError("Unreachable (strategy=" + codec.getStrategy() + ")");
}
/**
* Deserializes from {@code codedIn} using {@code codec}.
*
* <p>This extension point allows the implementation to optionally handle read futures and surface
* {@link DeferredValue}s, which are possible for {@link SharedValueDeserializationContext}.
*
* <p>This can return either a deserialized value or a {@link DeferredValue}. A {@link
* DeferredValue} is only possible for {@link SharedValueDeserializationContext}.
*/
@ForOverride
abstract Object deserializeAndMaybeHandleDeferredValues(
ObjectCodec<?> codec, CodedInputStream codedIn) throws SerializationException, IOException;
/**
* Corresponds to MemoBeforeContent in the abstract grammar.
*
* <p>May return a deserialized value or a {@link ListenableFuture}. The {@link ListenableFuture}
* is only possible for {@link SharedValueDeserializationContext}.
*/
private final Object deserializeMemoBeforeContent(ObjectCodec<?> codec, CodedInputStream codedIn)
throws SerializationException, IOException {
int tag = memoTable.size();
// During serialization, the top-level object is the first object to be memoized regardless of
// the codec implementation. During deserialization, the top-level object only becomes
// available after `registerInitialValue` is called and some codecs may perform deserialization
// operations prior to `registerInitialValue`. To keep the tags in sync with the size of
// the `memoTable`, adds a placeholder for the top-level object.
memoTable.put(tag, INITIAL_VALUE_PLACEHOLDER);
this.tagForMemoizedBefore = tag;
// `codec` is never a `DeferredObjectCodec` because those are `MEMOIZE_AFTER` so this is always
// the deserialized value instance and never a `DeferredValue`.
Object value = deserializeAndMaybeHandleDeferredValues(codec, codedIn);
Object initial = memoizedBeforeStackForSanityChecking.removeLast();
if (value != initial) {
// This indicates a bug in the particular codec subclass.
throw new SerializationException(
String.format(
"codec did not return the initial instance: %s but was %s with codec %s",
value, initial, codec));
}
Object combinedValue = combineValueWithReadFutures(value);
if (combinedValue != value) {
// If the combined value is different, it means that it is a ListenableFuture and there are
// are read futures for this value. The (partial) value for `tag` will already be memoized by
// `registerInitialValue` at this point.
//
// Any backreferences to the existing entry would be from cyclic children, which
// tautologically need to tolerate incomplete values anyway. However, any subsequent
// backreferences will observe the ListenableFuture and process it so that only complete
// values are consumed.
updateMemoEntry(tag, combinedValue);
return combinedValue;
}
return value;
}
/**
* Corresponds to MemoAfterContent in the abstract grammar.
*
* <p>May return either a deserialized value or a {@link ListenableFuture}. The {@link
* ListenableFuture} is only possible for {@link SharedValueDeserializationContext}.
*/
private final Object deserializeMemoAfterContent(ObjectCodec<?> codec, CodedInputStream codedIn)
throws SerializationException, IOException {
Object value =
combineValueWithReadFutures(deserializeAndMaybeHandleDeferredValues(codec, codedIn));
int tag = codedIn.readInt32();
// If deserializing the children caused the parent object itself to be deserialized due to
// a cycle, then there's now a memo entry for the parent. Reuse that object, discarding
// the one we were trying to construct here, so as to avoid creating duplicate objects in
// the object graph.
Object cyclicallyCreatedObject = memoTable.get(tag);
if (cyclicallyCreatedObject != null) {
return cyclicallyCreatedObject;
}
memoize(tag, value);
return value;
}
/**
* Incorporates read futures in the context together with {@code value}.
*
* <p>May return the deserialized value or a {@link ListenableFuture} that wraps the deserialized
* value. The {@link ListenableFuture} is only possible for {@link
* SharedValueDeserializationContext}.
*/
@ForOverride
abstract Object combineValueWithReadFutures(Object value);
/**
* Adds a new id → object maplet to the memo table.
*
* <p>It is an error if the value is already be present.
*/
private final void memoize(int id, Object value) {
Object prev = memoTable.put(id, checkNotNull(value));
if (prev != null) { // Avoid boxing int with checkArgument.
throw new IllegalArgumentException(
String.format(
"Tried to memoize id %s to object '%s', when it is already memoized to object"
+ " '%s'",
id, value, prev));
}
}
private void updateMemoEntry(int id, Object newValue) {
Object prev = memoTable.put(id, newValue);
checkState(prev != null, "Tried to update id %s but there was no previous entry", id);
}
private static final class MemoizingDeserializationContextImpl
extends MemoizingDeserializationContext {
private MemoizingDeserializationContextImpl(
ObjectCodecRegistry registry, ImmutableClassToInstanceMap<Object> dependencies) {
super(registry, dependencies);
}
@Override
public MemoizingDeserializationContext getFreshContext() {
return new MemoizingDeserializationContextImpl(getRegistry(), getDependencies());
}
@Override
Object deserializeAndMaybeHandleDeferredValues(ObjectCodec<?> codec, CodedInputStream codedIn)
throws SerializationException, IOException {
return codec.safeCast(codec.deserialize(this, codedIn));
}
@Override
@CanIgnoreReturnValue
Object combineValueWithReadFutures(Object value) {
return value;
}
}
private static final class PlaceholderValue {
private PlaceholderValue() {}
}
}