/* * Copyright 2000-2016 JetBrains s.r.o. * * 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.intellij.util.indexing.containers; import com.intellij.util.indexing.ValueContainer; import com.intellij.util.indexing.impl.DebugAssertions; import gnu.trove.TIntProcedure; import org.jetbrains.annotations.NotNull; import java.util.Arrays; import static com.intellij.util.indexing.impl.DebugAssertions.EXTRA_SANITY_CHECKS; /** * Class buffers changes in 2 modes: * - Accumulating up to MAX_FILES changes appending them *sequentially* to changes array * - Adding changes to randomAccessContainer once it is available: later happens if we accumulated many changes or external client queried * state of the changes: asked for predicate, iterator, isEmpty, etc. We are trying hard to delay transformation of state upon 2nd reason for * performance reasons. * It is assumed that add / remove operations as well as read only operations are externally synchronized, the only synchronization is * performed upon transforming changes array into randomAccessContainer because it can be done during read only operations in several threads */ public class ChangeBufferingList implements Cloneable { static final int MAX_FILES = 20000; // less than Short.MAX_VALUE //static final int MAX_FILES = 100; private volatile int[] changes; private short length; private boolean hasRemovals; private volatile boolean mayHaveDupes; private volatile RandomAccessIntContainer randomAccessContainer; private IdSet checkSet; public ChangeBufferingList() { this(3); } public ChangeBufferingList(int length) { if (length > MAX_FILES) { randomAccessContainer = new IdBitSet(length); } else { changes = new int[length]; } checkSet = EXTRA_SANITY_CHECKS ? new IdSet(length) : null; } @NotNull static int[] calcMinMax(int[] set, int length) { int max = Integer.MIN_VALUE; int min = Integer.MAX_VALUE; for(int i = 0; i < length; ++i) { max = Math.max(max, set[i]); min = Math.min(min, set[i]); } return new int[] {min, max}; } public void add(int value) { ensureCapacity(1); if (checkSet != null) checkSet.add(value); RandomAccessIntContainer intContainer = randomAccessContainer; if (intContainer == null) { addChange(value); } else { intContainer.add(value); } } private void addChange(int value) { if (value < 0) { if (!hasRemovals) hasRemovals = true; } else if (!mayHaveDupes && length > 0 && changes[length - 1] >= value) { mayHaveDupes = true; } changes[length++] = value; } public void remove(int value) { ensureCapacity(1); if (checkSet != null) checkSet.remove(value); RandomAccessIntContainer intContainer = randomAccessContainer; if (intContainer == null) { addChange(-value); } else { boolean removed = intContainer.remove(value); if (removed) intContainer.compact(); } } @Override public Object clone() { try { ChangeBufferingList clone = (ChangeBufferingList)super.clone(); if (changes != null) clone.changes = changes.clone(); if (randomAccessContainer != null) { clone.randomAccessContainer = (RandomAccessIntContainer)randomAccessContainer.clone(); } if (checkSet != null) clone.checkSet = (IdSet)checkSet.clone(); return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } private RandomAccessIntContainer getRandomAccessContainer() { int[] currentChanges = changes; if (currentChanges == null) return randomAccessContainer; synchronized (currentChanges) { currentChanges = changes; if (currentChanges == null) return randomAccessContainer; boolean copyChanges = true; RandomAccessIntContainer idSet; if (randomAccessContainer == null) { int someElementsNumberEstimation = length; // todo we can check these lengths instead of only relying upon reaching MAX_FILES //int lengthOfBitSet = IdBitSet.sizeInBytes(minMax[1], minMax[0]); //int lengthOfIntSet = 4 * length; if (someElementsNumberEstimation < MAX_FILES) { if (!hasRemovals) { if (mayHaveDupes) { removingDupesAndSort(); } idSet = new SortedIdSet(currentChanges, length); copyChanges = false; } else { idSet = new SortedIdSet(Math.max(someElementsNumberEstimation, 3)); } } else if (!hasRemovals) { idSet = new IdBitSet(changes, length, 0); copyChanges = false; } else { idSet = new IdBitSet(calcMinMax(changes, length), 0); } } else if (checkSet != null) { idSet = (RandomAccessIntContainer)randomAccessContainer.clone(); } else { idSet = randomAccessContainer; } assert idSet != null; if (copyChanges) { for(int i = 0, len = length; i < len; ++i) { int id = currentChanges[i]; if (id > 0) { idSet.add(id); } else { idSet.remove(-id); } } } if (checkSet != null) { DebugAssertions.assertTrue(checkSet.size() == idSet.size()); final RandomAccessIntContainer finalIdSet = idSet; checkSet.forEach(new TIntProcedure() { @Override public boolean execute(int value) { DebugAssertions.assertTrue(finalIdSet.contains(value)); return true; } }); } length = 0; hasRemovals = false; mayHaveDupes = false; randomAccessContainer = idSet; changes = null; return randomAccessContainer; } } private void removingDupesAndSort() { // duplicated ids can be present for some index due to cancellation of indexing for next index final int[] currentChanges = changes; final int intLength = length; if (intLength < 250) { // Plain sorting in Arrays works without allocations for small number of elements (see DualPivotQuicksort.QUICKSORT_THRESHOLD) Arrays.sort(currentChanges, 0, intLength); boolean hasDupes = false; for(int i = 0, max = intLength - 1; i < max; ++i) { if (currentChanges[i] == currentChanges[i + 1]) { hasDupes = true; break; } } if (hasDupes) { int ptr = 0; for(int i = 1; i < intLength; ++i) { if (currentChanges[i] != currentChanges[ptr]) { currentChanges[++ptr] = currentChanges[i]; } } length = (short)(ptr + 1); } } else { ValueContainer.IntIterator sorted = SortedFileIdSetIterator.getTransientIterator(new ChangesIterator(currentChanges, length, false)); int lastIndex = 0; while (sorted.hasNext()) { currentChanges[lastIndex++] = sorted.next(); } length = (short)lastIndex; } mayHaveDupes = false; } public void ensureCapacity(int diff) { RandomAccessIntContainer intContainer = randomAccessContainer; if (length == MAX_FILES) { intContainer = getRandomAccessContainer(); // transform into more compact storage } if (intContainer != null) { randomAccessContainer = intContainer.ensureContainerCapacity(diff); return; } if (changes == null) { changes = new int[Math.max(3, diff)]; } else if (length + diff > changes.length) { int[] newChanges = new int[calcNextArraySize(changes.length, length + diff)]; System.arraycopy(changes, 0, newChanges, 0, length); changes = newChanges; } } static int calcNextArraySize(int currentSize, int wantedSize) { return Math.min( Math.max(currentSize < 1024 ? currentSize << 1 : currentSize + currentSize / 5, wantedSize), MAX_FILES ); } public boolean isEmpty() { if (randomAccessContainer == null) { if (changes == null) { if (checkSet != null) DebugAssertions.assertTrue(checkSet.isEmpty()); return true; } if (!hasRemovals) { boolean b = length == 0; if (checkSet != null) DebugAssertions.assertTrue(b == checkSet.isEmpty()); return b; } } // todo we can calculate isEmpty in more cases (without container) RandomAccessIntContainer intContainer = getRandomAccessContainer(); return intContainer.size() == 0; } public ValueContainer.IntPredicate intPredicate() { final ValueContainer.IntPredicate predicate = getRandomAccessContainer().intPredicate(); if (checkSet != null) { return new ValueContainer.IntPredicate() { @Override public boolean contains(int id) { boolean answer = predicate.contains(id); DebugAssertions.assertTrue(answer == checkSet.contains(id)); return answer; } }; } return predicate; } public IntIdsIterator intIterator() { RandomAccessIntContainer intContainer = randomAccessContainer; if (intContainer == null && !hasRemovals) { int[] currentChanges = changes; if (currentChanges != null) { if (mayHaveDupes) { synchronized (currentChanges) { if (mayHaveDupes) removingDupesAndSort(); } } return new ChangesIterator(currentChanges, length, true); } } return getRandomAccessContainer().intIterator(); } public IntIdsIterator sortedIntIterator() { IntIdsIterator intIterator = intIterator(); if (!intIterator.hasAscendingOrder()) { intIterator = SortedFileIdSetIterator.getTransientIterator(intIterator); } return intIterator; } public IdSet getCheckSet() { return checkSet; } private static class ChangesIterator implements IntIdsIterator { private int cursor; private final int length; private final int[] changes; private final boolean sorted; ChangesIterator(int[] _changes, int _length, boolean _sorted) { changes = _changes; length = _length; sorted = _sorted; } @Override public boolean hasNext() { return cursor < length; } @Override public int next() { int current = cursor; ++cursor; return changes[current]; } @Override public int size() { return length; } @Override public boolean hasAscendingOrder() { return sorted; } @Override public IntIdsIterator createCopyInInitialState() { return new ChangesIterator(changes, length, sorted); } } }