// 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 com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Interner; import com.google.common.collect.Interners; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.devtools.build.lib.collect.CompactHashSet; import com.google.devtools.build.lib.util.Preconditions; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.Nullable; /** * A utility class that allows us to keep the reverse dependencies as an array list instead of a * set. This is more memory-efficient. At the same time it allows us to group the removals and * uniqueness checks so that it also performs well. * *

The reason for making this a separate class is to share non-trivial code between BuildingState * and NodeEntry. We could simply make those two classes extend this class instead, but we would be * less memory-efficient since object memory alignment does not cross classes (you would have two * memory alignments, one for the base class and one for the extended one). * */ public abstract class ReverseDepsUtilImpl implements ReverseDepsUtil { static final int MAYBE_CHECK_THRESHOLD = 10; private static final Interner consolidateInterner = Interners.newWeakInterner(); abstract void setReverseDepsObject(T container, Object object); abstract void setDataToConsolidate(T container, @Nullable List dataToConsolidate); abstract Object getReverseDepsObject(T container); abstract List getDataToConsolidate(T container); private enum ConsolidateOp { CHECK, ADD, REMOVE } /** * Opaque container for a pending operation on the reverse deps set. We use subclasses to save * 8 bytes of memory instead of keeping a field in this class, and we store * {@link ConsolidateOp#CHECK} operations as the bare {@link SkyKey} in order to save the wrapper * object in that case. */ private abstract static class KeyToConsolidate { // Do not access directly -- use the {@link #key} static accessor instead. protected final SkyKey key; /** Do not call directly -- use the {@link #create} static method instead. */ private KeyToConsolidate(SkyKey key) { this.key = key; } @Override public String toString() { return MoreObjects.toStringHelper(this).add("key", key).toString(); } /** Gets which operation was delayed for the given object. */ static ConsolidateOp op(Object obj) { if (obj instanceof SkyKey) { return ConsolidateOp.CHECK; } if (obj instanceof KeyToRemove) { return ConsolidateOp.REMOVE; } Preconditions.checkState(obj instanceof KeyToAdd, obj); return ConsolidateOp.ADD; } /** Gets the key whose operation was delayed for the given object. */ static SkyKey key(Object obj) { if (obj instanceof SkyKey) { return (SkyKey) obj; } Preconditions.checkState(obj instanceof KeyToConsolidate, obj); return ((KeyToConsolidate) obj).key; } static Object create(SkyKey key, ConsolidateOp op) { switch (op) { case CHECK: return key; case REMOVE: return consolidateInterner.intern(new KeyToRemove(key)); case ADD: return consolidateInterner.intern(new KeyToAdd(key)); default: throw new IllegalStateException(op + ", " + key); } } @Override public boolean equals(Object obj) { if (obj == null) { return false; } return this.getClass() == obj.getClass() && this.key.equals(((KeyToConsolidate) obj).key); } @Override public int hashCode() { // Overridden in subclasses. throw new UnsupportedOperationException(key.toString()); } } private static final class KeyToAdd extends KeyToConsolidate { KeyToAdd(SkyKey key) { super(key); } @Override public int hashCode() { return key.hashCode(); } } private static final class KeyToRemove extends KeyToConsolidate { KeyToRemove(SkyKey key) { super(key); } @Override public int hashCode() { return 42 + 37 * key.hashCode(); } } private void maybeDelayReverseDepOp(T container, Iterable reverseDeps, ConsolidateOp op) { List consolidations = getDataToConsolidate(container); int currentReverseDepSize = getCurrentReverseDepSize(container); if (consolidations == null) { consolidations = new ArrayList<>(currentReverseDepSize); setDataToConsolidate(container, consolidations); } for (SkyKey reverseDep : reverseDeps) { consolidations.add(KeyToConsolidate.create(reverseDep, op)); } // TODO(janakr): Should we consolidate more aggressively? This threshold can be customized. if (consolidations.size() == currentReverseDepSize) { consolidateData(container); } } private boolean isSingleReverseDep(T container) { return !(getReverseDepsObject(container) instanceof List); } /** * We only check if reverse deps is small and there are no delayed data to consolidate, since * then presence or absence would not be known. */ @Override public void maybeCheckReverseDepNotPresent(T container, SkyKey reverseDep) { if (getDataToConsolidate(container) != null) { return; } if (isSingleReverseDep(container)) { Preconditions.checkState( !getReverseDepsObject(container).equals(reverseDep), "Reverse dep %s already present in %s", reverseDep, container); return; } @SuppressWarnings("unchecked") List asList = (List) getReverseDepsObject(container); if (asList.size() < MAYBE_CHECK_THRESHOLD) { Preconditions.checkState( !asList.contains(reverseDep), "Reverse dep %s already present in %s for %s", reverseDep, asList, container); } } private int getCurrentReverseDepSize(T container) { return isSingleReverseDep(container) ? 1 : ((List) getReverseDepsObject(container)).size(); } /** * We use a memory-efficient trick to keep reverseDeps memory usage low. Edges in Bazel are * dominant over the number of nodes. * *

Most of the nodes have zero or one reverse dep. That is why we use immutable versions of the * lists for those cases. In case of the size being > 1 we switch to an ArrayList. That is because * we also have a decent number of nodes for which the reverseDeps are huge (for example almost * everything depends on BuildInfo node). * *

We also optimize for the case where we have only one dependency. In that case we keep the * object directly instead of a wrapper list. */ @SuppressWarnings("unchecked") public void addReverseDeps(T container, Collection newReverseDeps) { if (newReverseDeps.isEmpty()) { return; } List dataToConsolidate = getDataToConsolidate(container); if (dataToConsolidate != null) { maybeDelayReverseDepOp(container, newReverseDeps, ConsolidateOp.ADD); return; } Object reverseDeps = getReverseDepsObject(container); int reverseDepsSize = isSingleReverseDep(container) ? 1 : ((List) reverseDeps).size(); int newSize = reverseDepsSize + newReverseDeps.size(); if (newSize == 1) { overwriteReverseDepsWithObject(container, Iterables.getOnlyElement(newReverseDeps)); } else if (reverseDepsSize == 0) { overwriteReverseDepsList(container, Lists.newArrayList(newReverseDeps)); } else if (reverseDepsSize == 1) { List newList = Lists.newArrayListWithExpectedSize(newSize); newList.add((SkyKey) reverseDeps); newList.addAll(newReverseDeps); overwriteReverseDepsList(container, newList); } else { ((List) reverseDeps).addAll(newReverseDeps); } } @Override public void checkReverseDep(T container, SkyKey reverseDep) { maybeDelayReverseDepOp(container, ImmutableList.of(reverseDep), ConsolidateOp.CHECK); } /** * See {@code addReverseDeps} method. */ @Override public void removeReverseDep(T container, SkyKey reverseDep) { maybeDelayReverseDepOp(container, ImmutableList.of(reverseDep), ConsolidateOp.REMOVE); } @Override public ImmutableSet getReverseDeps(T container) { consolidateData(container); // TODO(bazel-team): Unfortunately, we need to make a copy here right now to be on the safe side // wrt. thread-safety. The parents of a node get modified when any of the parents is deleted, // and we can't handle that right now. if (isSingleReverseDep(container)) { return ImmutableSet.of((SkyKey) getReverseDepsObject(container)); } else { @SuppressWarnings("unchecked") List reverseDeps = (List) getReverseDepsObject(container); ImmutableSet set = ImmutableSet.copyOf(reverseDeps); Preconditions.checkState( set.size() == reverseDeps.size(), "Duplicate reverse deps present in %s: %s. %s", this, reverseDeps, container); return set; } } @Override public void consolidateReverseDeps(T container) { consolidateData(container); } private void consolidateData(T container) { List dataToConsolidate = getDataToConsolidate(container); if (dataToConsolidate == null) { return; } setDataToConsolidate(container, null); Object reverseDeps = getReverseDepsObject(container); if (isSingleReverseDep(container)) { Preconditions.checkState( dataToConsolidate.size() == 1, "dataToConsolidate not size 1 even though only one rdep: %s %s %s", dataToConsolidate, reverseDeps, container); Object keyToConsolidate = Iterables.getOnlyElement(dataToConsolidate); SkyKey key = KeyToConsolidate.key(keyToConsolidate); switch (KeyToConsolidate.op(keyToConsolidate)) { case REMOVE: overwriteReverseDepsList(container, ImmutableList.of()); // Fall through to check. case CHECK: Preconditions.checkState( key.equals(reverseDeps), "%s %s %s", keyToConsolidate, reverseDeps, container); break; case ADD: throw new IllegalStateException( "Shouldn't delay add if only one element: " + keyToConsolidate + ", " + reverseDeps + ", " + container); default: throw new IllegalStateException(keyToConsolidate + ", " + reverseDeps + ", " + container); } return; } List reverseDepsAsList = (List) reverseDeps; Set reverseDepsAsSet = CompactHashSet.create(reverseDepsAsList); if (reverseDepsAsSet.size() != reverseDepsAsList.size()) { // We're about to crash. Try to print an informative error message. Set seen = new HashSet<>(); List duplicates = new ArrayList<>(); for (SkyKey key : reverseDepsAsList) { if (!seen.add(key)) { duplicates.add(key); } } throw new IllegalStateException( (reverseDepsAsList.size() - reverseDepsAsSet.size()) + " duplicates: " + duplicates + " for " + container); } for (Object keyToConsolidate : dataToConsolidate) { SkyKey key = KeyToConsolidate.key(keyToConsolidate); switch (KeyToConsolidate.op(keyToConsolidate)) { case CHECK: Preconditions.checkState( reverseDepsAsSet.contains(key), "%s %s %s", keyToConsolidate, reverseDepsAsSet, container); break; case REMOVE: Preconditions.checkState( reverseDepsAsSet.remove(key), "%s %s %s", keyToConsolidate, reverseDeps, container); break; case ADD: Preconditions.checkState( reverseDepsAsSet.add(key), "%s %s %s", keyToConsolidate, reverseDeps, container); break; default: throw new IllegalStateException( keyToConsolidate + ", " + reverseDepsAsSet + ", " + container); } } if (reverseDepsAsSet.isEmpty()) { overwriteReverseDepsList(container, ImmutableList.of()); } else if (reverseDepsAsSet.size() == 1) { overwriteReverseDepsWithObject(container, Iterables.getOnlyElement(reverseDepsAsSet)); } else { overwriteReverseDepsList(container, new ArrayList<>(reverseDepsAsSet)); } } @Override public String toString(T container) { return MoreObjects.toStringHelper("ReverseDeps") .add("reverseDeps", getReverseDepsObject(container)) .add("singleReverseDep", isSingleReverseDep(container)) .add("dataToConsolidate", getDataToConsolidate(container)) .toString(); } private void overwriteReverseDepsWithObject(T container, SkyKey newObject) { setReverseDepsObject(container, newObject); } private void overwriteReverseDepsList(T container, List list) { setReverseDepsObject(container, list); } }