| // 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.checkNotNull; | |
| import static com.google.common.base.Preconditions.checkState; | |
| import com.google.common.collect.ForwardingConcurrentMap; | |
| import com.google.common.collect.ImmutableSortedMap; | |
| import com.google.common.collect.Maps; | |
| import com.google.devtools.build.lib.cmdline.Label; | |
| import com.google.devtools.build.lib.cmdline.Label.LabelInterner; | |
| import com.google.devtools.build.lib.cmdline.PackageIdentifier; | |
| import com.google.devtools.build.lib.concurrent.PooledInterner; | |
| import com.google.devtools.build.lib.packages.Target; | |
| import com.google.devtools.build.lib.profiler.AutoProfiler; | |
| import com.google.devtools.build.lib.profiler.GoogleAutoProfilerUtils; | |
| import com.google.devtools.build.lib.skyframe.PackageValue; | |
| import com.google.devtools.build.lib.skyframe.SkyFunctions; | |
| import com.google.devtools.build.lib.supplier.InterruptibleSupplier; | |
| import com.google.devtools.build.skyframe.SkyKey.SkyKeyInterner; | |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; | |
| import com.google.errorprone.annotations.ForOverride; | |
| import java.time.Duration; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ConcurrentMap; | |
| import java.util.concurrent.locks.Lock; | |
| import java.util.function.Consumer; | |
| import javax.annotation.Nullable; | |
| /** | |
| * An in-memory graph implementation. All operations are thread-safe with ConcurrentMap semantics. | |
| * Also see {@link NodeEntry}. | |
| * | |
| * <p>This class is public only for use in alternative graph implementations. | |
| */ | |
| public class InMemoryGraphImpl implements InMemoryGraph { | |
| private static final int PARALLELISM_THRESHOLD = 1024; | |
| // Use ForwardingConcurrentMap as a live reference to nodeMap so that it's safe to mutate | |
| // nodeMap and not have callers see the stale map. See getValues, getDoneValues, | |
| // getAllNodeEntries. | |
| private final ConcurrentMap<SkyKey, InMemoryNodeEntry> liveView = | |
| new ForwardingConcurrentMap<>() { | |
| @Override | |
| protected ConcurrentMap<SkyKey, InMemoryNodeEntry> delegate() { | |
| return nodeMap; | |
| } | |
| }; | |
| protected ConcurrentHashMap<SkyKey, InMemoryNodeEntry> nodeMap; | |
| private final NodeBatch getBatch; | |
| private final NodeBatch createIfAbsentBatch; | |
| private final boolean usePooledInterning; | |
| InMemoryGraphImpl() { | |
| this(/* initialCapacity= */ 1 << 10); | |
| } | |
| /** | |
| * For some shell integration tests, we don't want to apply {@link SkyKeyInterner} created and | |
| * bind {@code SkyKeyInterner#globalPool} to the second {@link InMemoryGraph}. | |
| */ | |
| InMemoryGraphImpl(boolean usePooledInterning) { | |
| this(/* initialCapacity= */ 1 << 10, usePooledInterning); | |
| } | |
| protected InMemoryGraphImpl(int initialCapacity) { | |
| this(initialCapacity, /* usePooledInterning= */ true); | |
| } | |
| private InMemoryGraphImpl(int initialCapacity, boolean usePooledInterning) { | |
| this.nodeMap = new ConcurrentHashMap<>(initialCapacity); | |
| this.getBatch = this::getIfPresent; | |
| this.createIfAbsentBatch = this::createIfAbsent; | |
| this.usePooledInterning = usePooledInterning; | |
| if (usePooledInterning) { | |
| SkyKeyInterner.setGlobalPool(new SkyKeyPool()); | |
| LabelInterner.setGlobalPool(new LabelPool()); | |
| } | |
| } | |
| @Override | |
| public void remove(SkyKey skyKey) { | |
| weakInternSkyKey(skyKey); | |
| InMemoryNodeEntry nodeEntry = nodeMap.remove(skyKey); | |
| if (skyKey instanceof PackageIdentifier && nodeEntry != null) { | |
| weakInternPackageTargetsLabels( | |
| (PackageValue) nodeEntry.toValue()); // Dirty or changed value are needed. | |
| } | |
| } | |
| @Override | |
| public void removeIfDone(SkyKey key) { | |
| nodeMap.computeIfPresent( | |
| key, | |
| (k, e) -> { | |
| if (e.isDone()) { | |
| weakInternSkyKey(k); | |
| if (k instanceof PackageIdentifier) { | |
| weakInternPackageTargetsLabels((PackageValue) e.toValue()); | |
| } | |
| return null; | |
| } | |
| return e; | |
| }); | |
| } | |
| private void weakInternSkyKey(SkyKey skyKey) { | |
| if (!usePooledInterning) { | |
| return; | |
| } | |
| SkyKeyInterner<?> interner = skyKey.getSkyKeyInterner(); | |
| if (interner != null) { | |
| interner.weakInternUnchecked(skyKey); | |
| } | |
| } | |
| private void weakInternPackageTargetsLabels(@Nullable PackageValue packageValue) { | |
| if (!usePooledInterning || packageValue == null) { | |
| return; | |
| } | |
| LabelInterner interner = Label.getLabelInterner(); | |
| ImmutableSortedMap<String, Target> targets = packageValue.getPackage().getTargets(); | |
| targets.values().forEach(t -> interner.weakIntern(t.getLabel())); | |
| } | |
| @Override | |
| public NodeEntry get(@Nullable SkyKey requestor, Reason reason, SkyKey skyKey) { | |
| return nodeMap.get(skyKey); | |
| } | |
| @Override | |
| public NodeBatch getBatch( | |
| @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) { | |
| return getBatch; | |
| } | |
| @Override | |
| public InterruptibleSupplier<NodeBatch> getBatchAsync( | |
| @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) { | |
| return () -> getBatch; | |
| } | |
| @Override | |
| public Map<SkyKey, NodeEntry> getBatchMap( | |
| SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) { | |
| // Use a HashMap, not an ImmutableMap.Builder, because we have not yet deduplicated these keys | |
| // and ImmutableMap.Builder does not tolerate duplicates. The map will be thrown away shortly. | |
| HashMap<SkyKey, NodeEntry> result = new HashMap<>(); | |
| for (SkyKey key : keys) { | |
| NodeEntry entry = get(null, Reason.OTHER, key); | |
| if (entry != null) { | |
| result.put(key, entry); | |
| } | |
| } | |
| return result; | |
| } | |
| @ForOverride | |
| protected InMemoryNodeEntry newNodeEntry(SkyKey key) { | |
| return new IncrementalInMemoryNodeEntry(key); | |
| } | |
| @Override | |
| @CanIgnoreReturnValue | |
| public NodeBatch createIfAbsentBatch( | |
| @Nullable SkyKey requestor, Reason reason, Iterable<? extends SkyKey> keys) { | |
| for (SkyKey key : keys) { | |
| createIfAbsent(key); | |
| } | |
| return createIfAbsentBatch; | |
| } | |
| @CanIgnoreReturnValue | |
| private InMemoryNodeEntry createIfAbsent(SkyKey skyKey) { | |
| SkyKeyInterner<?> interner = skyKey.getSkyKeyInterner(); | |
| if (!usePooledInterning || interner == null) { | |
| return nodeMap.computeIfAbsent(skyKey, this::newNodeEntry); | |
| } | |
| // The key is typically already present. Record whether this thread newly created a node so that | |
| // we can skip calling removeWeak if it was already present. | |
| boolean[] newlyCreated = new boolean[1]; | |
| InMemoryNodeEntry nodeEntry = | |
| nodeMap.computeIfAbsent( | |
| skyKey, | |
| k -> { | |
| newlyCreated[0] = true; | |
| return newNodeEntry(k); | |
| }); | |
| if (newlyCreated[0]) { | |
| interner.removeWeak(skyKey); | |
| } | |
| return nodeEntry; | |
| } | |
| @Override | |
| public DepsReport analyzeDepsDoneness(SkyKey parent, List<SkyKey> deps) { | |
| return DepsReport.NO_INFORMATION; | |
| } | |
| @Override | |
| public int valuesSize() { | |
| return nodeMap.size(); | |
| } | |
| @Override | |
| public Map<SkyKey, SkyValue> getValues() { | |
| return Collections.unmodifiableMap(Maps.transformValues(liveView, InMemoryNodeEntry::toValue)); | |
| } | |
| @Override | |
| public Map<SkyKey, SkyValue> getDoneValues() { | |
| return Collections.unmodifiableMap( | |
| Maps.filterValues( | |
| Maps.transformValues(liveView, entry -> entry.isDone() ? entry.getValue() : null), | |
| Objects::nonNull)); | |
| } | |
| @Override | |
| public Collection<InMemoryNodeEntry> getAllNodeEntries() { | |
| return Collections.unmodifiableCollection(nodeMap.values()); | |
| } | |
| @Override | |
| public void parallelForEach(Consumer<InMemoryNodeEntry> consumer) { | |
| nodeMap.forEachValue(PARALLELISM_THRESHOLD, consumer); | |
| } | |
| @Override | |
| public void cleanupInterningPools() { | |
| if (!usePooledInterning) { | |
| return; | |
| } | |
| try (AutoProfiler ignored = | |
| GoogleAutoProfilerUtils.logged("Cleaning up interning pools", Duration.ofMillis(2L))) { | |
| parallelForEach( | |
| e -> { | |
| weakInternSkyKey(e.getKey()); | |
| if (e.isDone() && e.getKey().functionName().equals(SkyFunctions.PACKAGE)) { | |
| weakInternPackageTargetsLabels((PackageValue) e.toValue()); | |
| } | |
| // The graph is about to be thrown away. Remove as we go to avoid temporarily storing | |
| // everything in both the weak interner and the graph. | |
| nodeMap.remove(e.getKey()); | |
| }); | |
| } | |
| SkyKeyInterner.setGlobalPool(null); | |
| LabelInterner.setGlobalPool(null); | |
| } | |
| @Override | |
| @Nullable | |
| public InMemoryNodeEntry getIfPresent(SkyKey key) { | |
| return nodeMap.get(key); | |
| } | |
| /** Minimizes the size of the ConcurrentHashMap backing the graph. May be costly to run (O(n)). */ | |
| @Override | |
| public void shrinkNodeMap() { | |
| nodeMap = new ConcurrentHashMap<>(nodeMap); | |
| } | |
| static final class EdgelessInMemoryGraphImpl extends InMemoryGraphImpl { | |
| public EdgelessInMemoryGraphImpl(boolean usePooledInterning) { | |
| super(usePooledInterning); | |
| } | |
| @Override | |
| protected InMemoryNodeEntry newNodeEntry(SkyKey key) { | |
| return new NonIncrementalInMemoryNodeEntry(key); | |
| } | |
| } | |
| /** {@link PooledInterner.Pool} for {@link SkyKey}s. */ | |
| final class SkyKeyPool implements PooledInterner.Pool<SkyKey> { | |
| @Override | |
| public SkyKey getOrWeakIntern(SkyKey sample) { | |
| // Use computeIfAbsent not to mutate the map, but to call weakIntern under synchronization. | |
| // This ensures that the canonical instance isn't being transferred to the node map | |
| // concurrently in createIfAbsent. In the common case that the key is already present in the | |
| // node map, this is a lock-free lookup. | |
| SkyKey[] weakInterned = new SkyKey[1]; | |
| InMemoryNodeEntry nodeEntry = | |
| nodeMap.computeIfAbsent( | |
| sample, | |
| k -> { | |
| weakInterned[0] = k.getSkyKeyInterner().weakInternUnchecked(k); | |
| return null; // Don't actually store a mapping. | |
| }); | |
| return nodeEntry != null ? nodeEntry.getKey() : weakInterned[0]; | |
| } | |
| } | |
| /** {@link PooledInterner.Pool} for {@link Label}s. */ | |
| final class LabelPool implements PooledInterner.Pool<Label> { | |
| @Override | |
| public Label getOrWeakIntern(Label sample) { | |
| LabelInterner interner = checkNotNull(Label.getLabelInterner()); | |
| PackageIdentifier packageIdentifier = sample.getPackageIdentifier(); | |
| // Return pooled instance if sample is present in the pool. | |
| InMemoryNodeEntry inMemoryNodeEntry = nodeMap.get(packageIdentifier); | |
| if (inMemoryNodeEntry != null) { | |
| Label pooledInstance = getLabelFromInMemoryNodeEntry(inMemoryNodeEntry, sample); | |
| if (pooledInstance != null) { | |
| return pooledInstance; | |
| } | |
| } | |
| Lock readLock = interner.getLockForLabelLookup(sample); | |
| readLock.lock(); | |
| try { | |
| // Check again whether sample is already present in the pool inside critical section. | |
| if (inMemoryNodeEntry == null) { | |
| inMemoryNodeEntry = nodeMap.get(packageIdentifier); | |
| } | |
| if (inMemoryNodeEntry != null) { | |
| Label pooledInstance = getLabelFromInMemoryNodeEntry(inMemoryNodeEntry, sample); | |
| if (pooledInstance != null) { | |
| return pooledInstance; | |
| } | |
| } | |
| return interner.weakIntern(sample); | |
| } finally { | |
| readLock.unlock(); | |
| } | |
| } | |
| } | |
| @Nullable | |
| private static Label getLabelFromInMemoryNodeEntry( | |
| InMemoryNodeEntry inMemoryNodeEntry, Label sample) { | |
| checkNotNull(inMemoryNodeEntry); | |
| SkyValue value = inMemoryNodeEntry.toValue(); | |
| if (value == null) { | |
| return null; | |
| } | |
| checkState(value instanceof PackageValue, value); | |
| ImmutableSortedMap<String, Target> targets = ((PackageValue) value).getPackage().getTargets(); | |
| Target target = targets.get(sample.getName()); | |
| return target != null ? target.getLabel() : null; | |
| } | |
| } |