package com.onthegomap.planetiler.collection; import static com.onthegomap.planetiler.util.MemoryEstimator.estimateSize; import com.carrotsearch.hppc.LongArrayList; import com.carrotsearch.hppc.LongIntHashMap; import com.onthegomap.planetiler.stats.Timer; import com.onthegomap.planetiler.util.DiskBacked; import com.onthegomap.planetiler.util.MemoryEstimator; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.Arrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An in-memory map that stores a multiple {@code long} values for each {@code long} key. *

* Implementations extend {@link Replaceable} if they support replacing the previous set of values for a key and/or * {@link Appendable} if they support adding new values for a key. */ public interface LongLongMultimap extends MemoryEstimator.HasEstimate, DiskBacked, AutoCloseable { /** Returns a {@link Noop} implementation that does nothing on put and throws an exception if you try to get. */ static Noop noop() { return new Noop(); } /** Returns a new multimap where each write sets the list of values for a key, and that order is preserved on read. */ static Replaceable newReplaceableMultimap(Storage storage, Storage.Params params) { return new DenseOrderedMultimap(storage, params); } /** Returns a new replaceable multimap held in-memory. */ static Replaceable newInMemoryReplaceableMultimap() { return newReplaceableMultimap(Storage.RAM, null); } /** Returns a new multimap where each write adds a value for the given key. */ static Appendable newAppendableMultimap() { return new SparseUnorderedBinarySearchMultimap(); } /** * Returns a new longlong multimap from config strings. * * @param storage name of the {@link Storage} implementation to use * @param path where to store data (if mmap) * @param madvise whether to use linux madvise random to improve read performance * @return A longlong map instance * @throws IllegalArgumentException if {@code name} or {@code storage} is not valid */ static Replaceable newReplaceableMultimap(String storage, Path path, boolean madvise) { return newReplaceableMultimap(Storage.from(storage), new Storage.Params(path, madvise)); } /** * Returns the values for a key. Safe to be called by multiple threads after all values have been written. After the * first read, all writes will fail. */ LongArrayList get(long key); @Override void close(); @Override default long diskUsageBytes() { return 0L; } /** * A map from long to list of longs where you can use {@link #replaceValues(long, LongArrayList)} to set replace the * previous list of values with a new one. */ interface Replaceable extends LongLongMultimap { /** Replaces the previous list of values for {@code key} with {@code values}. */ void replaceValues(long key, LongArrayList values); } /** * A map from long to list of longs where you can use {@link #put(long, long)} or {@link #putAll(long, LongArrayList)} * to append values for a key. */ interface Appendable extends LongLongMultimap { /** * Writes the value for a key. Not thread safe! */ void put(long key, long value); default void putAll(long key, LongArrayList vals) { for (int i = 0; i < vals.size(); i++) { put(key, vals.get(i)); } } } /** Dummy implementation of a map that throws an exception from {@link #get(long)}. */ class Noop implements Replaceable, Appendable { @Override public void put(long key, long value) { // do nothing on update } @Override public LongArrayList get(long key) { throw new UnsupportedOperationException("get(key) not implemented"); } @Override public long estimateMemoryUsageBytes() { return 0; } @Override public void close() { // nothing to close } @Override public void replaceValues(long key, LongArrayList values) { // do nothing on update } } /** * A map from {@code long} to {@code long} stored as a list of keys and values that uses binary search to find the * values for a key. Inserts do not need to be ordered, the first read will sort the array. */ class SparseUnorderedBinarySearchMultimap implements Appendable { private static final Logger LOGGER = LoggerFactory.getLogger(SparseUnorderedBinarySearchMultimap.class); private static final LongArrayList EMPTY_LIST = new LongArrayList(); private final LongArrayList keys = new LongArrayList(); private final LongArrayList values = new LongArrayList(); private volatile boolean prepared = false; @Override public void put(long key, long val) { if (val <= 0) { throw new IllegalArgumentException("Invalid value: " + val + " must be >0"); } if (prepared) { throw new IllegalArgumentException("Cannot insert after preparing"); } keys.add(key); values.add(val); } private void prepare() { if (!prepared) { synchronized (this) { if (!prepared) { doPrepare(); prepared = true; } } } } /** Sort the keys and values arrays by key */ private void doPrepare() { Timer timer = Timer.start(); LOGGER.debug("Sorting long long multimap..."); long[] sortedKeys = keys.toArray(); // this happens in a worker thread, but it's OK to use parallel sort because // all other threads will block while we prepare the multimap. Arrays.parallelSort(sortedKeys); // after sorting keys, sort values by iterating through each unordered key/value pair and // using binary search to find where to insert the result in sorted values. long[] sortedValues = new long[sortedKeys.length]; int from = 0; while (from < keys.size()) { long key = keys.get(from); int to = Arrays.binarySearch(sortedKeys, key); if (to < 0) { throw new IllegalStateException("Key not found: " + key); } // skip back to the first entry for this key while (to >= 0 && sortedKeys[to] == key) { to--; } // skip ahead past values we've already added for this key do { to++; } while (sortedValues[to] != 0); while (from < keys.size() && keys.get(from) == key) { sortedValues[to++] = values.get(from++); } } keys.buffer = sortedKeys; values.buffer = sortedValues; LOGGER.debug("Sorted long long multimap {}", timer.stop()); } @Override public LongArrayList get(long key) { prepare(); if (keys.isEmpty()) { return EMPTY_LIST; } int size = keys.size(); int index = Arrays.binarySearch(keys.buffer, 0, size, key); LongArrayList result = new LongArrayList(); if (index >= 0) { // binary search might drop us in the middle of repeated values, so look forwards... for (int i = index; i < size && keys.get(i) == key; i++) { result.add(values.get(i)); } // ... and backwards to get all the matches for (int i = index - 1; i >= 0 && keys.get(i) == key; i--) { result.add(values.get(i)); } } return result; } @Override public long estimateMemoryUsageBytes() { return estimateSize(keys) + estimateSize(values); } @Override public void close() { keys.release(); values.release(); } } /** * A map from {@code long} to {@code long} where each putAll replaces previous values and results are returned in the * same order they were inserted. */ class DenseOrderedMultimap implements Replaceable { private static final LongArrayList EMPTY_LIST = new LongArrayList(); private final LongIntHashMap keyToValuesIndex = Hppc.newLongIntHashMap(); // each block starts with a "length" header then contains that number of entries private final AppendStore.Longs values; public DenseOrderedMultimap(Storage storage, Storage.Params params) { values = switch (storage) { case MMAP -> new AppendStoreMmap.Longs(params); case RAM -> new AppendStoreRam.Longs(false); case DIRECT -> new AppendStoreRam.Longs(true); }; } @Override public void replaceValues(long key, LongArrayList values) { if (values.isEmpty()) { return; } keyToValuesIndex.put(key, (int) this.values.size()); this.values.appendLong(values.size()); for (int i = 0; i < values.size(); i++) { this.values.appendLong(values.get(i)); } } @Override public LongArrayList get(long key) { int index = keyToValuesIndex.getOrDefault(key, -1); if (index >= 0) { LongArrayList result = new LongArrayList(); int num = (int) values.getLong(index); for (int i = 0; i < num; i++) { result.add(values.getLong(i + index + 1L)); } return result; } else { return EMPTY_LIST; } } @Override public long estimateMemoryUsageBytes() { return estimateSize(keyToValuesIndex) + estimateSize(values); } @Override public long diskUsageBytes() { return values.diskUsageBytes(); } @Override public void close() { keyToValuesIndex.release(); try { values.close(); } catch (IOException e) { throw new UncheckedIOException(e); } } } }