io.opentelemetry.sdk.metrics.internal.state.PooledHashMap Maven / Gradle / Ivy
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.sdk.metrics.internal.state;
import static java.util.Objects.requireNonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
/**
* A bucket-based hash map with an internal re-usable map entry objects pool
*
* The goal of this map is to minimize memory allocation, leading to reduced time spent in
* garbage collection.
*
*
This map avoids allocating a new map entry on each put operation by maintaining a pool of
* reusable (mutable) map entries and borrowing a map entry object from the pool to hold the given
* key-value of the put operation. The borrowed object is returned to the pool when the map entry
* key is removed from the map.
*
*
This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*
*
This class is not thread-safe.
*
* @param The map key type
* @param The map value type
*/
@SuppressWarnings("ForLoopReplaceableByForEach")
public final class PooledHashMap implements Map {
private static final int DEFAULT_CAPACITY = 16;
private static final float LOAD_FACTOR = 0.75f;
private ArrayList>[] table;
private final ObjectPool> entryPool;
private int size;
/**
* Creates a {@link PooledHashMap} with {@code capacity} buckets.
*
* The hashmap contains an array of buckets, each is an array-list of items. The number of
* buckets expands over time to avoid having too many items in one bucket, otherwise accessing an
* item by key won't be a constant time complexity.
*
* @param capacity The initial number of buckets to start with
*/
@SuppressWarnings({"unchecked"})
public PooledHashMap(int capacity) {
this.table = (ArrayList>[]) new ArrayList>[capacity];
this.entryPool = new ObjectPool<>(Entry::new);
this.size = 0;
}
/**
* Creates a new {@link PooledHashMap} with a default amount of buckets (capacity).
*
* @see PooledHashMap#PooledHashMap(int)
*/
public PooledHashMap() {
this(DEFAULT_CAPACITY);
}
/**
* Add a key, value pair to the map.
*
* Internally it uses a MapEntry from a pool of entries, to store this mapping
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return Null if the was no previous mapping for this key, or the value of the previous mapping
* of this key
*/
@Override
@Nullable
public V put(K key, V value) {
requireNonNull(key, "This map does not support null keys");
requireNonNull(value, "This map does not support null values");
if (size > LOAD_FACTOR * table.length) {
rehash();
}
int bucket = getBucket(key);
ArrayList> entries = table[bucket];
if (entries == null) {
entries = new ArrayList<>();
table[bucket] = entries;
} else {
// Don't optimize to enhanced for-loop since implicit iterator used allocated memory in O(n)
for (int i = 0; i < entries.size(); i++) {
Entry entry = entries.get(i);
if (Objects.equals(entry.key, key)) {
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
}
}
Entry entry = entryPool.borrowObject();
entry.key = key;
entry.value = value;
entries.add(entry);
size++;
return null;
}
@SuppressWarnings({"unchecked"})
private void rehash() {
ArrayList>[] oldTable = table;
table = (ArrayList>[]) new ArrayList>[2 * oldTable.length];
// put() to new table below will reset size back to correct number
size = 0;
for (int i = 0; i < oldTable.length; i++) {
ArrayList> bucket = oldTable[i];
if (bucket != null) {
for (Entry entry : bucket) {
put(requireNonNull(entry.key), requireNonNull(entry.value));
entryPool.returnObject(entry);
}
bucket.clear();
}
}
}
/**
* Retrieves the mapped value for {@code key}.
*
* @param key the key whose associated value is to be returned
* @return The mapped value for {@code key} or null if there is no such mapping
*/
@Override
@Nullable
@SuppressWarnings("unchecked")
public V get(Object key) {
requireNonNull(key, "This map does not support null keys");
int bucket = getBucket((K) key);
ArrayList> entries = table[bucket];
if (entries != null) {
for (int i = 0; i < entries.size(); i++) {
Entry entry = entries.get(i);
if (Objects.equals(entry.key, key)) {
return entry.value;
}
}
}
return null;
}
/**
* Removes the mapping for the given {@code key}.
*
* @param key key whose mapping is to be removed from the map
* @return The value mapped to this key, if the mapping exists, or null otherwise
*/
@Override
@Nullable
@SuppressWarnings("unchecked")
public V remove(Object key) {
requireNonNull(key, "This map does not support null keys");
int bucket = getBucket((K) key);
ArrayList> entries = table[bucket];
if (entries != null) {
for (int i = 0; i < entries.size(); i++) {
Entry entry = entries.get(i);
if (Objects.equals(entry.key, key)) {
V oldValue = entry.value;
entries.remove(i);
entryPool.returnObject(entry);
size--;
return oldValue;
}
}
}
return null;
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean containsKey(Object key) {
requireNonNull(key, "This map does not support null keys");
return get(key) != null;
}
@Override
public boolean containsValue(Object value) {
throw new UnsupportedOperationException();
}
@Override
public void clear() {
for (int i = 0; i < table.length; i++) {
ArrayList> bucket = table[i];
if (bucket != null) {
for (int j = 0; j < bucket.size(); j++) {
Entry entry = bucket.get(j);
entryPool.returnObject(entry);
}
bucket.clear();
}
}
size = 0;
}
@Override
public void forEach(BiConsumer super K, ? super V> action) {
for (int j = 0; j < table.length; j++) {
ArrayList> bucket = table[j];
if (bucket != null) {
for (int i = 0; i < bucket.size(); i++) {
Entry entry = bucket.get(i);
action.accept(entry.key, entry.value);
}
}
}
}
private int getBucket(K key) {
return Math.abs(key.hashCode() % table.length);
}
@Override
public Set> entrySet() {
throw new UnsupportedOperationException();
}
@Override
public Collection values() {
throw new UnsupportedOperationException();
}
@Override
public void putAll(Map extends K, ? extends V> m) {
throw new UnsupportedOperationException();
}
@Override
public Set keySet() {
throw new UnsupportedOperationException();
}
private static class Entry {
@Nullable K key;
@Nullable V value;
}
}