/** * Copyright (C) 2014-2016 LinkedIn Corp. (pinot-core@linkedin.com) * * 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.linkedin.pinot.core.query.aggregation.groupby; import com.google.common.base.Preconditions; import com.linkedin.pinot.common.utils.Pairs.IntObjectPair; import com.linkedin.pinot.core.util.IntObjectIndexedPriorityQueue; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; /** * Result Holder implemented using ObjectArray. */ public class ObjectGroupByResultHolder implements GroupByResultHolder { private final int _maxCapacity; private final int _trimSize; private final boolean _minHeap; private int _resultHolderCapacity; private StorageMode _storageMode; private Object[] _resultArray; private Int2ObjectOpenHashMap _resultMap; private IntObjectIndexedPriorityQueue _priorityQueue; /** * Constructor for the class. * * @param initialCapacity Initial capacity of result holder * @param maxCapacity Max capacity of result holder * @param trimSize maximum number of groups returned after trimming. * @param minOrder Min ordering for trim (in case of min aggregation functions) */ public ObjectGroupByResultHolder(int initialCapacity, int maxCapacity, int trimSize, boolean minOrder) { _resultArray = new Object[initialCapacity]; _resultHolderCapacity = initialCapacity; _storageMode = StorageMode.ARRAY_STORAGE; _maxCapacity = maxCapacity; _trimSize = trimSize; _minHeap = !minOrder; // Max order requires minHeap for trimming results, and vice-versa _resultMap = null; } /** * Constructor for the class, assumes max ordering for trim. * * @param initialCapacity Initial capacity of result holder * @param maxCapacity Max capacity of result holder * @param trimSize maximum number of groups returned after trimming. */ public ObjectGroupByResultHolder(int initialCapacity, int maxCapacity, int trimSize) { this(initialCapacity, maxCapacity, trimSize, false /* minOrdering */); } /** * {@inheritDoc} * * @param capacity */ @Override public void ensureCapacity(int capacity) { Preconditions.checkArgument(capacity <= _maxCapacity); // Nothing to be done for map mode. if (_storageMode == StorageMode.MAP_STORAGE) { return; } // If object is not comparable, we cannot use a priority queue and cannot compare. if (capacity > _trimSize && (_resultArray[0] instanceof Comparable)) { switchToMapMode(capacity); return; } if (capacity > _resultHolderCapacity) { int copyLength = _resultHolderCapacity; _resultHolderCapacity = Math.max(_resultHolderCapacity * 2, capacity); // Cap the growth to maximum possible number of group keys _resultHolderCapacity = Math.min(_resultHolderCapacity, _maxCapacity); Object[] current = _resultArray; _resultArray = new Object[_resultHolderCapacity]; System.arraycopy(current, 0, _resultArray, 0, copyLength); } } /** * {@inheritDoc} * * Array based result holder assumes group by key fit within integer. * This is a valid assumption as ArrayBasedResultHolder gets instantiated * iff groupKey are less than 1M. * * @param groupKey * @return */ @Override public double getDoubleResult(int groupKey) { throw new RuntimeException( "Unsupported method getDoubleResult (returning double) for class " + getClass().getName()); } @Override @SuppressWarnings("unchecked") public <T> T getResult(int groupKey) { return (T) ((_storageMode == StorageMode.ARRAY_STORAGE) ? _resultArray[groupKey] : _resultMap.get(groupKey)); } /** * {@inheritDoc} * * @param groupKey * @param newValue */ @Override public void setValueForKey(int groupKey, double newValue) { throw new RuntimeException( "Unsupported method 'putValueForKey' (with double param) for class " + getClass().getName()); } @SuppressWarnings("unchecked") @Override public void setValueForKey(int groupKey, Object newValue) { if (_storageMode == StorageMode.ARRAY_STORAGE) { _resultArray[groupKey] = newValue; } else { _resultMap.put(groupKey, newValue); _priorityQueue.put(groupKey, (Comparable) newValue); } } /** * {@inheritDoc} * * Keys with 'lowest' values (as per the sort order) are trimmed away to reduce the size to _trimSize. * * @return Array of keys that were trimmed. */ @Override public int[] trimResults() { if (_storageMode == StorageMode.ARRAY_STORAGE) { return EMPTY_ARRAY; // Still in array mode, trimming has not kicked in yet. } int numKeysToRemove = _resultMap.size() - _trimSize; int[] removedGroupKeys = new int[numKeysToRemove]; for (int i = 0; i < numKeysToRemove; i++) { IntObjectPair pair = _priorityQueue.poll(); int groupKey = pair.getIntValue(); _resultMap.remove(groupKey); removedGroupKeys[i] = groupKey; } return removedGroupKeys; } /** * Helper method to switch the storage from array mode to map mode. * * @param initialPriorityQueueSize Initial size of priority queue */ @SuppressWarnings("unchecked") private void switchToMapMode(int initialPriorityQueueSize) { _storageMode = StorageMode.MAP_STORAGE; _resultMap = new Int2ObjectOpenHashMap(_resultArray.length); _priorityQueue = new IntObjectIndexedPriorityQueue(initialPriorityQueueSize, _minHeap); for (int id = 0; id < _resultArray.length; id++) { _resultMap.put(id, _resultArray[id]); _priorityQueue.put(id, (Comparable) _resultArray[id]); } _resultArray = null; } }