| // Copyright 2017 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.collect; |
| |
| import com.google.common.base.Objects; |
| import com.google.common.base.Preconditions; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Interner; |
| import com.google.devtools.build.lib.concurrent.BlazeInterners; |
| import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; |
| import com.google.errorprone.annotations.CanIgnoreReturnValue; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import javax.annotation.concurrent.Immutable; |
| |
| /** |
| * Provides a memory-efficient map when the key sets are likely to be shared between multiple |
| * instances of this class. |
| * |
| * <p>This class is appropriate where it is expected that a lot of the key sets will be the same. |
| * These key sets are shared and an offset table of indices is computed. Each map instance thus |
| * contains only a reference to the shared offset table, and a plain array of instances. |
| * |
| * <p>The map is sensitive to insertion order. Two maps with different insertion orders are *not* |
| * considered equal, and will not share keys. |
| * |
| * <p>This class explicitly does *not* implement the Map interface, as use of that would lead to a |
| * lot of GC churn. |
| */ |
| @Immutable |
| public class ImmutableSharedKeyMap<K, V> extends CompactImmutableMap<K, V> { |
| private static final Interner<OffsetTable<?>> offsetTables = BlazeInterners.newWeakInterner(); |
| |
| private final OffsetTable<K> offsetTable; |
| @VisibleForSerialization protected final Object[] values; |
| |
| private static final class OffsetTable<K> { |
| private final Object[] keys; |
| // Keep a map around to speed up get lookups for larger maps. |
| // We make this value lazy to avoid computing for values that end up being thrown away |
| // during interning anyway (the majority). |
| private volatile ImmutableMap<K, Integer> indexMap; |
| |
| private OffsetTable(Object[] keys) { |
| this.keys = keys; |
| } |
| |
| void initIndexMap() { |
| if (indexMap == null) { |
| synchronized (this) { |
| if (indexMap == null) { |
| ImmutableMap.Builder<K, Integer> builder = ImmutableMap.builder(); |
| for (int i = 0; i < keys.length; ++i) { |
| @SuppressWarnings("unchecked") |
| K key = (K) keys[i]; |
| builder.put(key, i); |
| } |
| this.indexMap = builder.buildOrThrow(); |
| } |
| } |
| } |
| } |
| |
| private ImmutableMap<K, Integer> getIndexMap() { |
| return indexMap; |
| } |
| |
| int offsetForKey(K key) { |
| return getIndexMap().getOrDefault(key, -1); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (!(o instanceof OffsetTable)) { |
| return false; |
| } |
| OffsetTable<?> that = (OffsetTable<?>) o; |
| return Arrays.equals(this.keys, that.keys); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Arrays.hashCode(keys); |
| } |
| } |
| |
| protected ImmutableSharedKeyMap(Object[] keys, Object[] values) { |
| Preconditions.checkArgument(keys.length == values.length); |
| this.values = values; |
| this.offsetTable = createOffsetTable(keys); |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static <K> OffsetTable<K> createOffsetTable(Object[] keys) { |
| OffsetTable<K> offsetTable = new OffsetTable<>(keys); |
| OffsetTable<K> internedTable = (OffsetTable<K>) offsetTables.intern(offsetTable); |
| internedTable.initIndexMap(); |
| return internedTable; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public V get(K key) { |
| int offset = offsetTable.offsetForKey(key); |
| return offset != -1 ? (V) values[offset] : null; |
| } |
| |
| @Override |
| public int size() { |
| return values.length; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public K keyAt(int index) { |
| return (K) offsetTable.keys[index]; |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public V valueAt(int index) { |
| return (V) values[index]; |
| } |
| |
| /** Do not use! Present only for serialization. (Annotated as @Deprecated just to prevent use.) */ |
| @Deprecated |
| @VisibleForSerialization |
| public Object[] getKeys() { |
| return offsetTable.keys; |
| } |
| |
| @Override |
| @SuppressWarnings("ReferenceEquality") |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| ImmutableSharedKeyMap<?, ?> that = (ImmutableSharedKeyMap<?, ?>) o; |
| // We can use object identity for the offset table due to |
| // it being interned |
| return offsetTable == that.offsetTable && Arrays.equals(values, that.values); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(offsetTable, Arrays.hashCode(values)); |
| } |
| |
| public static <K, V> Builder<K, V> builder() { |
| return new Builder<>(); |
| } |
| |
| /** Builder for {@link ImmutableSharedKeyMap}. */ |
| public static class Builder<K, V> { |
| private final List<Object> entries = new ArrayList<>(); |
| |
| private Builder() {} |
| |
| @CanIgnoreReturnValue |
| public Builder<K, V> put(K key, V value) { |
| entries.add(key); |
| entries.add(value); |
| return this; |
| } |
| |
| public ImmutableSharedKeyMap<K, V> build() { |
| int count = entries.size() / 2; |
| Object[] keys = new Object[count]; |
| Object[] values = new Object[count]; |
| int entryIndex = 0; |
| for (int i = 0; i < count; ++i) { |
| keys[i] = entries.get(entryIndex++); |
| values[i] = entries.get(entryIndex++); |
| } |
| return new ImmutableSharedKeyMap<>(keys, values); |
| } |
| } |
| } |