blob: e9d1ebfa7888812dd503a4997e4a1ef272d119c3 [file] [log] [blame]
// Copyright 2014 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.skyframe;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.collect.compacthashset.CompactHashSet;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.checkerframework.framework.qual.DefaultQualifierInHierarchy;
import org.checkerframework.framework.qual.LiteralKind;
import org.checkerframework.framework.qual.QualifierForLiterals;
import org.checkerframework.framework.qual.SubtypeOf;
/**
* Encapsulates Skyframe dependencies, preserving the groups in which they were requested.
*
* <p>This class itself does no duplicate checking, although it is expected that a {@code
* GroupedDeps} instance contains no duplicates - Skyframe is responsible for only adding keys which
* are not already present.
*
* <p>{@link #equals} is sensitive the order of groups, but is insensitive to the order of elements
* within a group.
*/
public class GroupedDeps implements Iterable<List<SkyKey>> {
/**
* Indicates that the annotated element is a compressed {@link GroupedDeps}, so that it can be
* safely passed to {@link #decompress} and friends.
*/
@SubtypeOf(DefaultObject.class)
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@QualifierForLiterals(LiteralKind.NULL)
public @interface Compressed {}
/** Default annotation for type-safety checks of {@link Compressed}. */
@DefaultQualifierInHierarchy
@SubtypeOf({})
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
private @interface DefaultObject {}
/** The total number of deps. */
private int size;
/**
* The deps and group delimiters. Each element is either a {@link SkyKey} or {@link Integer}.
* Integers represent the start of a new group and indicate how many elements are in the group.
* Singleton groups have no preceding integer.
*
* <p>The group sizes are redundant with the indices stored in {@link #groupIndices}, but they are
* stored here nonetheless so that {@link #compress} can simply convert this list to an array.
*/
private final ArrayList<Object> elements;
/**
* Indices into {@link #elements} for each group, maintained to provide constant time access to
* groups in {@link #getDepGroup}. The first group has no entry in this list since it always
* starts at index 0. Otherwise, the starting index for group {@code i} in {@link #elements} is
* stored in this list at index {@code i - 1}. For multi-element groups, the starting index refers
* to the position of the {@link Integer} representing the size of the group.
*/
private final ArrayList<Integer> groupIndices;
private final CollectionView collectionView = new CollectionView();
public GroupedDeps() {
this(0, newSmallArrayList());
}
private GroupedDeps(int size, ArrayList<Object> elements) {
this(size, elements, newSmallArrayList());
}
private GroupedDeps(int size, ArrayList<Object> elements, ArrayList<Integer> groupIndices) {
this.size = size;
this.elements = elements;
this.groupIndices = groupIndices;
}
/**
* Creates a new {@link ArrayList} with single-element capacity.
*
* <p>Many Skyframe nodes have only 0 or 1 dep. Pre-sizing small reduces garbage.
*/
private static <T> ArrayList<T> newSmallArrayList() {
return new ArrayList<>(1);
}
/**
* Adds a new group with a single element.
*
* <p>The caller must ensure that the given element is not already present.
*/
public void appendSingleton(SkyKey key) {
markNextGroup();
elements.add(key);
size++;
}
/**
* Adds a new group.
*
* <p>The caller must ensure that the new group is duplicate-free and does not contain any
* elements which are already present.
*/
public void appendGroup(List<SkyKey> group) {
appendNextGroup(group.iterator(), group.size());
}
/**
* Adds possibly many new groups.
*
* <p>The iteration order of the given deps along with the {@code groupSizes} parameter dictate
* how deps are grouped. For example, if {@code deps = {a,b,c}} and {@code groupSizes = [2, 1]},
* then there will be two groups: {@code [a,b]} and {@code [c]}. The sum of {@code groupSizes}
* must equal the size of {@code deps}. Note that it only makes sense to call this method with a
* set implementation that has a stable iteration order.
*
* <p>The caller must ensure that the given set of deps does not contain any elements which are
* already present.
*/
public void appendGroups(Set<SkyKey> deps, List<Integer> groupSizes) {
elements.ensureCapacity(elements.size() + deps.size());
if (isEmpty()) {
groupIndices.ensureCapacity(groupSizes.size() - 1);
} else {
groupIndices.ensureCapacity(groupIndices.size() + groupSizes.size());
}
Iterator<SkyKey> it = deps.iterator();
for (Integer size : groupSizes) {
appendNextGroup(it, size);
}
checkArgument(
!it.hasNext(), "size(deps) != sum(groupSizes) (deps=%s, groupSizes=%s)", deps, groupSizes);
}
private void appendNextGroup(Iterator<SkyKey> it, Integer groupSize) {
if (groupSize == 0) {
return;
}
if (groupSize == 1) {
appendSingleton(it.next());
return;
}
markNextGroup();
elements.ensureCapacity(elements.size() + groupSize + 1);
elements.add(groupSize);
for (int i = 0; i < groupSize; i++) {
elements.add(it.next());
}
size += groupSize;
}
private void markNextGroup() {
if (!isEmpty()) {
groupIndices.add(elements.size());
}
}
/**
* Removes the elements in {@code toRemove} from this {@code GroupedDeps}. Takes time proportional
* to the number of deps, so should not be called often.
*
* <p>Should not be called during iteration.
*/
public void remove(Set<SkyKey> toRemove) {
if (toRemove.isEmpty()) {
return;
}
GroupedDeps newDeps = new GroupedDeps();
for (List<SkyKey> group : this) {
List<SkyKey> newGroup = new ArrayList<>(group.size());
for (SkyKey key : group) {
if (!toRemove.contains(key)) {
newGroup.add(key);
}
}
newDeps.appendGroup(newGroup);
}
checkArgument(
newDeps.size == size - toRemove.size(),
"Requested removal of absent element(s) (toRemove=%s, elements=%s)",
toRemove,
elements);
size = newDeps.size;
elements.clear();
elements.addAll(newDeps.elements);
groupIndices.clear();
groupIndices.addAll(newDeps.groupIndices);
}
/**
* Returns the group at position {@code i} as an unmodifiable list.
*
* <p>The returned list is a live view of the backing list, so should not be used after a
* subsequent call to {@link #remove}.
*/
@SuppressWarnings("unchecked") // Cast of sublist containing only SkyKeys to List<SkyKey>.
public List<SkyKey> getDepGroup(int i) {
int index = i == 0 ? 0 : groupIndices.get(i - 1);
Object obj = elements.get(index);
if (obj instanceof SkyKey) {
return ImmutableList.of((SkyKey) obj);
}
int groupSize = (int) obj;
List<?> slice = elements.subList(index + 1, index + 1 + groupSize);
return (List<SkyKey>) Collections.unmodifiableList(slice);
}
/** Returns the number of dependency groups. */
public int numGroups() {
return isEmpty() ? 0 : groupIndices.size() + 1;
}
/**
* Returns the number of individual dependencies, as opposed to the number of groups -- equivalent
* to adding up the sizes of each dependency group.
*/
public int numElements() {
return size;
}
public static int numElements(@Compressed Object compressed) {
switch (compressionCase(compressed)) {
case EMPTY:
return 0;
case SINGLETON:
return 1;
case MULTIPLE:
Object[] arr = (Object[]) compressed;
int size = 0;
int i = 0;
while (i < arr.length) {
Object obj = arr[i++];
if (obj instanceof SkyKey) {
size++;
} else {
int groupSize = (int) obj;
size += groupSize;
i += groupSize;
}
}
return size;
}
throw new AssertionError(compressed);
}
private enum CompressionCase {
EMPTY,
SINGLETON,
MULTIPLE
}
private static CompressionCase compressionCase(@Compressed Object compressed) {
if (compressed == EMPTY_COMPRESSED) {
return CompressionCase.EMPTY;
}
if (compressed instanceof SkyKey) {
return CompressionCase.SINGLETON;
}
checkArgument(compressed.getClass().isArray(), compressed);
return CompressionCase.MULTIPLE;
}
/**
* Converts a compressed {@code GroupedDeps} into an {@link Iterable}. Equivalent to calling
* {@link #decompress} and then {@link #getAllElementsAsIterable}, but more efficient.
*/
public static Iterable<SkyKey> compressedToIterable(@Compressed Object compressed) {
switch (compressionCase(compressed)) {
case EMPTY:
return ImmutableList.of();
case SINGLETON:
return ImmutableList.of((SkyKey) compressed);
case MULTIPLE:
List<Object> elements = Arrays.asList((Object[]) compressed);
return () -> new UngroupedIterator(elements);
}
throw new AssertionError(compressed);
}
/**
* Casts an {@code Object} which is known to be {@link Compressed}.
*
* <p>This method should only be used when it is not possible to enforce the type via annotations.
*/
public static @Compressed Object castAsCompressed(Object obj) {
checkArgument(obj == EMPTY_COMPRESSED || obj instanceof SkyKey || obj.getClass().isArray());
return (@Compressed Object) obj;
}
/** Returns true if this list contains no elements. */
public boolean isEmpty() {
return elements.isEmpty();
}
/** Determines whether the given compressed {@code GroupedDeps} is empty. */
public static boolean isEmpty(@Compressed Object compressed) {
return compressed == EMPTY_COMPRESSED;
}
/**
* Returns true if this list contains the given key. May take time proportional to list size. Call
* {@link #toSet} instead and use the result if doing multiple contains checks and this is not a
* {@link WithHashSet}.
*/
public boolean contains(SkyKey key) {
return elements.contains(key);
}
@SerializationConstant @AutoCodec.VisibleForSerialization
static final @Compressed Object EMPTY_COMPRESSED = new Object();
/**
* Returns a memory-efficient representation of dependency groups.
*
* <p>The compressed representation does not support mutation or random access to dep groups. If
* this functionality is needed, use {@link #decompress}.
*/
public @Compressed Object compress() {
switch (numElements()) {
case 0:
return EMPTY_COMPRESSED;
case 1:
return elements.get(0);
default:
return elements.toArray();
}
}
public ImmutableSet<SkyKey> toSet() {
ImmutableSet.Builder<SkyKey> builder = ImmutableSet.builderWithExpectedSize(size);
for (Object obj : elements) {
if (obj instanceof SkyKey) {
builder.add((SkyKey) obj);
}
}
return builder.build();
}
/** Reconstitutes a compressed representation returned by {@link #compress}. */
public static GroupedDeps decompress(@Compressed Object compressed) {
switch (compressionCase(compressed)) {
case EMPTY:
return new GroupedDeps();
case SINGLETON:
return new GroupedDeps(1, Lists.newArrayList(compressed));
case MULTIPLE:
// Count the size and reconstruct groupIndices.
Object[] arr = (Object[]) compressed;
int size = 0;
ArrayList<Integer> groupIndices = newSmallArrayList();
int i = 0;
while (i < arr.length) {
if (i > 0) {
groupIndices.add(i);
}
Object obj = arr[i++];
if (obj instanceof SkyKey) {
size++;
} else {
int groupSize = (int) obj;
size += groupSize;
i += groupSize;
}
}
return new GroupedDeps(size, Lists.newArrayList(arr), groupIndices);
}
throw new AssertionError(compressed);
}
@Override
public int hashCode() {
// Hashing requires getting an order-independent hash for each element of this.elements. That
// is too expensive for a hash code.
throw new UnsupportedOperationException("Should not need to get hash for " + this);
}
/**
* Checks that two dep groups (neither of which may contain duplicates) have the same elements,
* regardless of order.
*/
private static boolean checkUnorderedEqualityOfGroups(List<SkyKey> group1, List<SkyKey> group2) {
if (group1.size() != group2.size()) {
return false;
}
// The order-sensitive comparison usually returns true. When it does, the CompactHashSet doesn't
// need to be constructed.
return group1.equals(group2) || CompactHashSet.create(group1).containsAll(group2);
}
/**
* A grouping-unaware view which does not support modifications.
*
* <p>This is implemented as a {@code Collection} so that calling {@link Iterables#size} on the
* return value of {@link #getAllElementsAsIterable} will take constant time.
*/
private final class CollectionView extends AbstractCollection<SkyKey> {
@Override
public Iterator<SkyKey> iterator() {
return new UngroupedIterator(elements);
}
@Override
public int size() {
return size;
}
}
/** An iterator that loops through every element in each group. */
private static final class UngroupedIterator implements Iterator<SkyKey> {
private final List<Object> elements;
private int i = 0;
private UngroupedIterator(List<Object> elements) {
this.elements = elements;
advanceIfSizeMarker();
}
@Override
public boolean hasNext() {
return i < elements.size();
}
@Override
public SkyKey next() {
SkyKey next = (SkyKey) elements.get(i++);
advanceIfSizeMarker();
return next;
}
private void advanceIfSizeMarker() {
if (i < elements.size() && elements.get(i) instanceof Integer) {
i++;
}
}
}
@ThreadHostile
public Collection<SkyKey> getAllElementsAsIterable() {
return collectionView;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GroupedDeps)) {
return false;
}
GroupedDeps that = (GroupedDeps) other;
// Fast paths for inequality.
if (this.size != that.size
|| this.elements.size() != that.elements.size()
|| this.numGroups() != that.numGroups()) {
return false;
}
// We must check the deps, ignoring the ordering of deps in the same group.
Iterator<List<SkyKey>> thisIt = this.iterator();
Iterator<List<SkyKey>> thatIt = that.iterator();
while (thisIt.hasNext()) {
if (!checkUnorderedEqualityOfGroups(thisIt.next(), thatIt.next())) {
return false;
}
}
return true;
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("size", size).add("elements", elements).toString();
}
/**
* Iterator that returns the next group in elements for each call to {@link #next}. A custom
* iterator is needed here because, to optimize memory, we store single-element lists as elements
* internally, and so they must be wrapped before they're returned.
*/
private class GroupedIterator implements Iterator<List<SkyKey>> {
private int i = 0;
@Override
public boolean hasNext() {
return i < numGroups();
}
@Override
public List<SkyKey> next() {
return getDepGroup(i++);
}
}
@Override
public Iterator<List<SkyKey>> iterator() {
return new GroupedIterator();
}
/**
* A {@link GroupedDeps} which keeps a {@link HashSet} of its elements up to date, resulting in a
* higher memory cost and faster {@link #contains} operations.
*/
public static final class WithHashSet extends GroupedDeps {
private final HashSet<SkyKey> set = new HashSet<>();
@Override
public void appendSingleton(SkyKey key) {
super.appendSingleton(key);
set.add(key);
}
@Override
public void appendGroup(List<SkyKey> group) {
super.appendGroup(group);
set.addAll(group);
}
@Override
public void appendGroups(Set<SkyKey> deps, List<Integer> groupSizes) {
super.appendGroups(deps, groupSizes);
set.addAll(deps);
}
@Override
public void remove(Set<SkyKey> toRemove) {
super.remove(toRemove);
set.removeAll(toRemove);
}
@Override
public boolean contains(SkyKey needle) {
return set.contains(needle);
}
@Override
public ImmutableSet<SkyKey> toSet() {
return ImmutableSet.copyOf(set);
}
}
}