// 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 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. * *

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. * *

The map is sensitive to insertion order. Two maps with different insertion orders are *not* * considered equal, and will not share keys. * *

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 extends CompactImmutableMap { private static final Interner offsetTables = BlazeInterners.newWeakInterner(); private final OffsetTable offsetTable; @VisibleForSerialization protected final Object[] values; private static final class OffsetTable { 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 indexMap; private OffsetTable(Object[] keys) { this.keys = keys; } void initIndexMap() { if (indexMap == null) { synchronized (this) { if (indexMap == null) { ImmutableMap.Builder 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.build(); } } } } private ImmutableMap 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 OffsetTable createOffsetTable(Object[] keys) { OffsetTable offsetTable = new OffsetTable<>(keys); OffsetTable internedTable = (OffsetTable) 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 Builder builder() { return new Builder<>(); } /** Builder for {@link ImmutableSharedKeyMap}. */ public static class Builder { private final List entries = new ArrayList<>(); private Builder() {} public Builder put(K key, V value) { entries.add(key); entries.add(value); return this; } public ImmutableSharedKeyMap 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); } } }