| // Copyright 2021 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.packages; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.UnmodifiableIterator; |
| import java.util.AbstractMap; |
| import java.util.AbstractSet; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A bimap with the following features and restrictions: |
| * |
| * <ul> |
| * <li>it (lazily) tracks the order in which keys were inserted; |
| * <li>... but only for entries whose values satisfy a predicate; |
| * <li>it's append-only, i.e. it supports addition of new key-value pairs, or replacement of the |
| * value of an existing key, but not deletion of key-value pairs; |
| * <li>... with the restriction that replacement is not allowed to make a previously tracked entry |
| * become untracked. |
| * </ul> |
| * |
| * <p>Tracking the insertion order and prohibiting key deletion allows this bimap to provide a |
| * lightweight snapshot view for iterating (in key insertion order) over entries which existed at a |
| * given point in time. |
| * |
| * <p>Intended to be used by {@code native.existing_rules} in Starlark, which needs to be able to |
| * iterate, at some later point in time, over the rules which existed in a {@link Package.Builder} |
| * at the time of the {@code existing_rules} call. We do not want to track insertion orders of |
| * numerous non-rule targets (e.g. files) - hence, filtering by a predicate. And in the common case |
| * where {@code existing_rules} is never called in a package, we want to avoid the overhead of |
| * keeping track of insertion orders - hence, laziness. |
| * |
| * <p>In packages with a large number of targets, the use of lightweight snapshots instead of |
| * copying results in a noticeable improvement in loading times, e.g. 2.2 times faster loading for a |
| * package with 4400 targets and 300 {@code native.existing_rules} calls. |
| */ |
| final class SnapshottableBiMap<K, V> implements BiMap<K, V> { |
| private final BiMap<K, V> contents = HashBiMap.create(); |
| private final Predicate<V> track; |
| |
| // trackedKeys and trackedKeyOrders are initialized lazily by ensureOrderTracking(). In the case |
| // where the order-tracking map represents a package builder's targets, ensureOrderTracking() is |
| // intended to be triggered only by a call to {@code native.existing_rules} in Starlark. |
| // |
| // Holds all keys being tracked, in their relative insertion order. |
| private ArrayList<K> trackedKeys; |
| // Maps all keys being tracked to their index in trackedKeys. |
| private Map<K, Integer> trackedKeyOrders; |
| |
| public SnapshottableBiMap(Predicate<V> track) { |
| this.track = track; |
| } |
| |
| /** |
| * Returns the underlying contents bimap. |
| * |
| * <p>Mutating the underlying bimap will violate the guarantees of this class and possibly cause |
| * inconsistent behavior in snapshot views. Therefore, the recommended usage pattern is to replace |
| * any references to the {@code SnapshottableBiMap} with the underlying map, and ensure that any |
| * snapshots of the map are no longer in use at that point. |
| * |
| * <p>An optimization hack intended only for use from {@link Package.Builder#beforeBuild}. |
| */ |
| BiMap<K, V> getUnderlyingBiMap() { |
| return contents; |
| } |
| |
| @Override |
| public int size() { |
| return contents.size(); |
| } |
| |
| private int sizeTracked() { |
| ensureOrderTracking(); |
| return trackedKeyOrders.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return contents.isEmpty(); |
| } |
| |
| @Override |
| public boolean containsKey(Object key) { |
| return contents.containsKey(key); |
| } |
| |
| @Override |
| public boolean containsValue(Object value) { |
| return contents.containsValue(value); |
| } |
| |
| @Override |
| @Nullable |
| public V get(Object key) { |
| return contents.get(key); |
| } |
| |
| /** |
| * Returns the insertion order of the specified key (relative to other tracked keys), or -1 if the |
| * key was never inserted into the map or corresponds to a key-value pair whose insertion order we |
| * do not track. Replacing a key's value does not change this order if tracking has already begun. |
| */ |
| private int getTrackedKeyOrder(Object key) { |
| ensureOrderTracking(); |
| Integer order = trackedKeyOrders.get(key); |
| return order == null ? -1 : order; |
| } |
| |
| /** |
| * Returns the tracked key with the specified insertion order (as determined by {@link |
| * #getTrackedKeyOrder}). |
| * |
| * @throws IndexOutOfBoundsException if the specified insertion order is out of bounds |
| */ |
| private K getTrackedKey(int order) { |
| ensureOrderTracking(); |
| return trackedKeys.get(order); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Note that once key insertion order tracking has started, overriding a key with a different |
| * value will not change the key's insertion order. |
| * |
| * @throws IllegalArgumentException if attempting to replace a key-value pair whose insertion |
| * order was tracked with a key-value pair whose insertion order is not tracked, or if the |
| * given value is already bound to a different key in this map. |
| */ |
| @Override |
| @Nullable |
| public V put(K key, V value) { |
| if (startedOrderTracking()) { |
| boolean oldWasTracked = getTrackedKeyOrder(key) >= 0; |
| boolean newIsTracked = track.test(value); |
| if (oldWasTracked) { |
| Preconditions.checkArgument( |
| newIsTracked, |
| "Cannot replace a key-value pair which is tracked with a key-value pair which is" |
| + " not tracked"); |
| } else { |
| if (newIsTracked) { |
| recordKeyOrder(key); |
| } |
| } |
| } |
| return contents.put(key, value); |
| } |
| |
| /** |
| * @deprecated Not supported, since it's morally equivalent to preceding a {@link #put} call with |
| * a silent {@code this.values().remove(value)}. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| @Nullable |
| public V forcePut(K key, V value) { |
| throw new UnsupportedOperationException("Append-only data structure"); |
| } |
| |
| /** |
| * @deprecated Not supported. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| @Nullable |
| public V remove(Object key) { |
| throw new UnsupportedOperationException("Append-only data structure"); |
| } |
| |
| @Override |
| public void putAll(Map<? extends K, ? extends V> map) { |
| for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) { |
| put(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| /** |
| * @deprecated Not supported. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public void clear() { |
| throw new UnsupportedOperationException("Append-only data structure"); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Removing a key from the set does not change the key's order if it was tracked prior to |
| * removal. Removal is supported only for consistency with {@link values}. |
| */ |
| @Override |
| public Set<K> keySet() { |
| return Collections.unmodifiableSet(contents.keySet()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Removing a value from the set does not change the key's order if it was tracked prior to |
| * removal. Ideally, we would not want to support removal, but it is required for {@link |
| * PackageFunction#handleLabelsCrossingSubpackagesAndPropagateInconsistentFilesystemExceptions}. |
| */ |
| @Override |
| public Set<V> values() { |
| return Collections.unmodifiableSet(contents.values()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>Removing an entry from the set does not change the key's order if it was tracked prior to |
| * removal. Removal is supported only for consistency with {@link values}. |
| */ |
| @Override |
| public Set<Map.Entry<K, V>> entrySet() { |
| return Collections.unmodifiableSet(contents.entrySet()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>The returned map is unmodifiable (all modifications will throw an {@link |
| * UnsupportedOperationException}. |
| */ |
| @Override |
| public BiMap<V, K> inverse() { |
| return Maps.unmodifiableBiMap(contents.inverse()); |
| } |
| |
| private boolean startedOrderTracking() { |
| Preconditions.checkState((trackedKeys == null) == (trackedKeyOrders == null)); |
| return trackedKeys != null; |
| } |
| |
| private void ensureOrderTracking() { |
| if (!startedOrderTracking()) { |
| trackedKeys = new ArrayList<>(); |
| trackedKeyOrders = new HashMap<>(); |
| |
| contents.forEach( |
| (key, value) -> { |
| if (track.test(value)) { |
| recordKeyOrder(key); |
| } |
| }); |
| } |
| } |
| |
| private void recordKeyOrder(K key) { |
| int order = trackedKeys.size(); |
| trackedKeys.add(key); |
| trackedKeyOrders.put(key, order); |
| } |
| |
| /** |
| * Returns a lightweight snapshot view of the tracked entries existing in the bimap at the time |
| * this method is called. |
| * |
| * <p>Most method calls on the view returned by this method will start insertion order tracking if |
| * it has not been started already. In particular, that implies that after this method had been |
| * called, a value whose insertion order was tracked may no longer be replaceable with a value |
| * whose insertion order is not tracked. See {@link #put} for details. |
| */ |
| public Map<K, V> getTrackedSnapshot() { |
| return new TrackedSnapshot<>(this); |
| } |
| |
| /** |
| * A view of a {@link SnapshottableBiMap}'s contents existing at a certain point in time. |
| * |
| * <p>Iterators over the view's {@link #keySet}, {@link #entrySet}, or {@link #values} iterate in |
| * key insertion order. |
| */ |
| static final class TrackedSnapshot<K, V> extends AbstractMap<K, V> { |
| private final SnapshottableBiMap<K, V> underlying; |
| // The number of initial elements from `underlying`'s `trackedKeys` list that should be |
| // considered to be present in this view. Note that we don't snapshot values, so we'll use |
| // whatever the most recent value in `underlying` is even if it changed after this snapshot |
| // was created. |
| private final int sizeTracked; |
| |
| private TrackedSnapshot(SnapshottableBiMap<K, V> underlying) { |
| this.underlying = underlying; |
| this.sizeTracked = underlying.sizeTracked(); |
| } |
| |
| @Override |
| public boolean containsKey(Object key) { |
| int order = underlying.getTrackedKeyOrder(key); |
| return order >= 0 && order < sizeTracked; |
| } |
| |
| @Override |
| public boolean containsValue(Object value) { |
| Object key = underlying.inverse().get(value); |
| if (key != null) { |
| int order = underlying.getTrackedKeyOrder(key); |
| return order >= 0 && order < sizeTracked; |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| @Nullable |
| public V get(Object key) { |
| if (containsKey(key)) { |
| return underlying.get(key); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| @Nullable |
| public V put(K key, V value) { |
| throw new UnsupportedOperationException("Read-only snapshot"); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| @Nullable |
| public V remove(Object key) { |
| throw new UnsupportedOperationException("Read-only snapshot"); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public void putAll(Map<? extends K, ? extends V> m) { |
| throw new UnsupportedOperationException("Read-only snapshot"); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public void clear() { |
| throw new UnsupportedOperationException("Read-only snapshot"); |
| } |
| |
| @Override |
| public Set<Map.Entry<K, V>> entrySet() { |
| return new UnmodifiableSet<Map.Entry<K, V>>() { |
| @Override |
| public int size() { |
| return sizeTracked; |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| return sizeTracked == 0; |
| } |
| |
| @Override |
| public boolean contains(Object object) { |
| if (!(object instanceof Map.Entry<?, ?>)) { |
| return false; |
| } |
| Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object; |
| return TrackedSnapshot.this.containsKey(entry.getKey()) |
| && TrackedSnapshot.this.containsValue(entry.getValue()); |
| } |
| |
| @Override |
| public Iterator<Map.Entry<K, V>> iterator() { |
| return new UnmodifiableIterator<Map.Entry<K, V>>() { |
| private int nextOrder = 0; |
| |
| @Override |
| public boolean hasNext() { |
| return nextOrder < TrackedSnapshot.this.sizeTracked; |
| } |
| |
| @Override |
| public Map.Entry<K, V> next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| K key = TrackedSnapshot.this.underlying.getTrackedKey(nextOrder); |
| V value = TrackedSnapshot.this.underlying.get(key); |
| nextOrder++; |
| return new AbstractMap.SimpleEntry<>(key, value); |
| } |
| }; |
| } |
| }; |
| } |
| |
| private abstract static class UnmodifiableSet<E> extends AbstractSet<E> { |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean add(E entry) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean remove(Object o) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Not implemented due to lack of need. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean containsAll(Collection<?> c) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean addAll(Collection<? extends E> c) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean retainAll(Collection<?> c) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public boolean removeAll(Collection<?> c) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * @deprecated Unsupported operation. |
| * @throws UnsupportedOperationException always. |
| */ |
| @Deprecated |
| @Override |
| public void clear() { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| } |
| } |