blob: 465a65a9df50e123e47eb452b34c607d0a5e9075 [file] [log] [blame]
/*
* Copyright 2000-2013 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.io;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
// Assigns / store unique integral id for Data instances.
// Btree stores mapping between integer hash code into integer that interpreted in following way:
// Positive value is address in myFile with unique key record.
// When there is hash value collisions the value is negative and it is -address of collision list (keyAddress, nextCollisionAddress)+
// It is possible to directly associate nonnegative int or long with Data instances when Data is integral value and represent it's own hash code
// e.g. Data are integers and hash code for them are values themselves
public class PersistentBTreeEnumerator<Data> extends PersistentEnumeratorBase<Data> {
private static final int PAGE_SIZE;
private static final int DEFAULT_PAGE_SIZE = 32768;
static {
int pageSize;
try {
String property = System.getProperty("idea.btree.page.size");
pageSize = property != null ? Integer.parseInt(property, 10):DEFAULT_PAGE_SIZE;
} catch (NumberFormatException ex) {
pageSize = DEFAULT_PAGE_SIZE;
}
PAGE_SIZE = pageSize;
}
private static final int RECORD_SIZE = 4;
private static final int VALUE_PAGE_SIZE = 1024 * 1024;
static {
assert VALUE_PAGE_SIZE % PAGE_SIZE == 0:"Page size should be divisor of " + VALUE_PAGE_SIZE;
}
private int myLogicalFileLength;
private int myDataPageStart;
private int myFirstPageStart;
private int myDataPageOffset;
private int myDuplicatedValuesPageStart;
private int myDuplicatedValuesPageOffset;
private static final int COLLISION_OFFSET = 4;
private int myValuesCount;
private int myCollisions;
private int myExistingKeysEnumerated;
private IntToIntBtree myBTree;
private final boolean myInlineKeysNoMapping;
private boolean myExternalKeysNoMapping;
private static final int DIRTY_MAGIC = 0xbabe1977;
private static final int VERSION = 7 + IntToIntBtree.version();
private static final int CORRECTLY_CLOSED_MAGIC = 0xebabafd + VERSION + PAGE_SIZE;
@NotNull private static final Version ourVersion = new Version(CORRECTLY_CLOSED_MAGIC, DIRTY_MAGIC);
private static final int KEY_SHIFT = 1;
public PersistentBTreeEnumerator(@NotNull File file, @NotNull KeyDescriptor<Data> dataDescriptor, int initialSize) throws IOException {
this(file, dataDescriptor, initialSize, null);
}
public PersistentBTreeEnumerator(@NotNull File file,
@NotNull KeyDescriptor<Data> dataDescriptor,
int initialSize,
@Nullable PagedFileStorage.StorageLockContext lockContext) throws IOException {
super(file,
new ResizeableMappedFile(
file,
initialSize,
lockContext,
VALUE_PAGE_SIZE,
true,
IOUtil.ourByteBuffersUseNativeByteOrder
),
dataDescriptor,
initialSize,
ourVersion,
new RecordBufferHandler(),
false
);
myInlineKeysNoMapping = myDataDescriptor instanceof InlineKeyDescriptor && !wantKeyMapping();
myExternalKeysNoMapping = !(myDataDescriptor instanceof InlineKeyDescriptor) && !wantKeyMapping();
if (myBTree == null) {
try {
lockStorage();
storeVars(false);
initBtree(false);
storeBTreeVars(false);
}
catch (IOException e) {
try {
close(); // cleanup already initialized state
}
catch (Throwable ignored) {
}
throw e;
}
catch (Throwable e) {
LOG.info(e);
try {
close(); // cleanup already initialized state
}
catch (Throwable ignored) {
}
throw new CorruptedException(file);
}
finally {
unlockStorage();
}
}
}
@NotNull
private File indexFile(@NotNull File file) {
return new File(file.getPath() + "_i");
}
protected boolean wantKeyMapping() {
return false;
}
private void initBtree(boolean initial) throws IOException {
myBTree = new IntToIntBtree(PAGE_SIZE, indexFile(myFile), myStorage.getPagedFileStorage().getStorageLockContext(), initial);
}
private void storeVars(boolean toDisk) {
myLogicalFileLength = store(DATA_START, myLogicalFileLength, toDisk);
myDataPageStart = store(DATA_START + 4, myDataPageStart, toDisk);
myDataPageOffset = store(DATA_START + 8, myDataPageOffset, toDisk);
myFirstPageStart = store(DATA_START + 12, myFirstPageStart, toDisk);
myDuplicatedValuesPageStart = store(DATA_START + 16, myDuplicatedValuesPageStart, toDisk);
myDuplicatedValuesPageOffset = store(DATA_START + 20, myDuplicatedValuesPageOffset, toDisk);
myValuesCount = store(DATA_START + 24, myValuesCount, toDisk);
myCollisions = store(DATA_START + 28, myCollisions, toDisk);
myExistingKeysEnumerated = store(DATA_START + 32, myExistingKeysEnumerated, toDisk);
storeBTreeVars(toDisk);
}
private void storeBTreeVars(boolean toDisk) {
final IntToIntBtree tree = myBTree;
if (tree != null) {
final int BTREE_DATA_START = DATA_START + 36;
tree.persistVars(new IntToIntBtree.BtreeDataStorage() {
@Override
public int persistInt(int offset, int value, boolean toDisk) {
return store(BTREE_DATA_START + offset, value, toDisk);
}
}, toDisk);
}
}
private int store(int offset, int value, boolean toDisk) {
assert offset + 4 < PAGE_SIZE;
if (toDisk) {
if (myFirstPageStart == -1 || myStorage.getInt(offset) != value) myStorage.putInt(offset, value);
} else {
value = myStorage.getInt(offset);
}
return value;
}
@Override
protected void setupEmptyFile() throws IOException {
myLogicalFileLength = PAGE_SIZE;
myFirstPageStart = myDataPageStart = -1;
myDuplicatedValuesPageStart = -1;
initBtree(true);
storeVars(true);
}
@Override
protected void doClose() throws IOException {
try {
super.doClose();
}
finally {
final IntToIntBtree tree = myBTree;
if (tree != null) {
tree.doClose();
}
}
}
private int allocPage() {
int pageStart = myLogicalFileLength;
myLogicalFileLength += PAGE_SIZE;
return pageStart;
}
@Override
public boolean processAllDataObject(@NotNull final Processor<Data> processor, @Nullable final DataFilter filter) throws IOException {
if(myInlineKeysNoMapping) {
return traverseAllRecords(new RecordsProcessor() {
@Override
public boolean process(final int record) throws IOException {
if (filter == null || filter.accept(record)) {
Data data = ((InlineKeyDescriptor<Data>)myDataDescriptor).fromInt(getCurrentKey());
return processor.process(data);
}
return true;
}
});
}
return super.processAllDataObject(processor, filter);
}
@Override
public boolean traverseAllRecords(@NotNull final RecordsProcessor p) throws IOException {
try {
lockStorage();
return myBTree.processMappings(new IntToIntBtree.KeyValueProcessor() {
@Override
public boolean process(int key, int value) throws IOException {
p.setCurrentKey(key);
if (value > 0) {
if (!p.process(value)) return false;
}
else {
int rec = -value;
while (rec != 0) {
int id = myStorage.getInt(rec);
if (!p.process(id)) return false;
rec = myStorage.getInt(rec + COLLISION_OFFSET);
}
}
return true;
}
});
}
catch (IllegalStateException e) {
CorruptedException corruptedException = new CorruptedException(myFile);
corruptedException.initCause(e);
throw corruptedException;
} finally {
unlockStorage();
}
}
protected int addrToIndex(int addr) {
assert myExternalKeysNoMapping;
return addr + KEY_SHIFT;
}
@Override
protected int indexToAddr(int idx) {
if (myExternalKeysNoMapping) {
IntToIntBtree.myAssert(idx > 0);
return idx - KEY_SHIFT;
}
int anInt = myStorage.getInt(idx);
if (IntToIntBtree.doSanityCheck) {
IntToIntBtree.myAssert(anInt >= 0 || myDataDescriptor instanceof InlineKeyDescriptor);
}
return anInt;
}
@Override
protected int setupValueId(int hashCode, int dataOff) {
if (myExternalKeysNoMapping) return addrToIndex(dataOff);
final PersistentEnumeratorBase.RecordBufferHandler<PersistentEnumeratorBase> recordHandler = getRecordHandler();
final byte[] buf = recordHandler.getRecordBuffer(this);
// optimization for using putInt / getInt on aligned empty storage (our page always contains on storage ByteBuffer)
final int pos = recordHandler.recordWriteOffset(this, buf);
myStorage.ensureSize(pos + buf.length);
if (!myInlineKeysNoMapping) myStorage.putInt(pos, dataOff);
return pos;
}
@Override
public void setRecordHandler(@NotNull PersistentEnumeratorBase.RecordBufferHandler<PersistentEnumeratorBase> recordHandler) {
myExternalKeysNoMapping = false;
super.setRecordHandler(recordHandler);
}
@Override
public Data getValue(int keyId, int processingKey) throws IOException {
if (myInlineKeysNoMapping) {
return ((InlineKeyDescriptor<Data>)myDataDescriptor).fromInt(processingKey);
}
return super.getValue(keyId, processingKey);
}
private final int[] myResultBuf = new int[1];
public long getNonnegativeValue(Data key) throws IOException {
assert myInlineKeysNoMapping;
try {
lockStorage();
final boolean hasMapping = myBTree.get(((InlineKeyDescriptor<Data>)myDataDescriptor).toInt(key), myResultBuf);
if (!hasMapping) {
return NULL_ID;
}
return keyIdToNonnegattiveOffset(myResultBuf[0]);
}
catch (IllegalStateException e) {
CorruptedException exception = new CorruptedException(myFile);
exception.initCause(e);
throw exception;
} finally {
unlockStorage();
}
}
public long keyIdToNonnegattiveOffset(int value) {
if (value >= 0) return value;
return myStorage.getLong(-value);
}
public void putNonnegativeValue(Data key, long value) throws IOException {
assert value >= 0;
assert myInlineKeysNoMapping;
try {
lockStorage();
markDirty(true);
int intKey = ((InlineKeyDescriptor<Data>)myDataDescriptor).toInt(key);
if (value < Integer.MAX_VALUE) {
myBTree.put(intKey, (int) value);
} else {
int pos = nextLongValueRecord();
myStorage.putLong(pos, value);
myBTree.put(intKey, -pos);
}
} catch (IllegalStateException e) {
CorruptedException exception = new CorruptedException(myFile);
exception.initCause(e);
throw exception;
} finally {
unlockStorage();
}
}
private int nextLongValueRecord() {
assert myInlineKeysNoMapping;
if (myDuplicatedValuesPageStart == -1 || myDuplicatedValuesPageOffset == myBTree.pageSize) {
myDuplicatedValuesPageStart = allocPage();
myDuplicatedValuesPageOffset = 0;
}
int duplicatedValueOff = myDuplicatedValuesPageOffset;
myDuplicatedValuesPageOffset += 8; // size of long
return myDuplicatedValuesPageStart + duplicatedValueOff;
}
@Override
protected int enumerateImpl(final Data value, final boolean onlyCheckForExisting, boolean saveNewValue) throws IOException {
try {
lockStorage();
if (IntToIntBtree.doDump) System.out.println(value);
final int valueHC = myDataDescriptor.getHashCode(value);
final boolean hasMapping = myBTree.get(valueHC, myResultBuf);
if (!hasMapping && onlyCheckForExisting) {
return NULL_ID;
}
final int indexNodeValueAddress = hasMapping ? myResultBuf[0]:0;
int collisionAddress = NULL_ID;
boolean hasExistingData = false;
if (!myInlineKeysNoMapping) {
collisionAddress = NULL_ID;
if (indexNodeValueAddress > 0) {
// we found reference to no dupe key
if (isKeyAtIndex(value, indexNodeValueAddress)) {
if (!saveNewValue) {
++myExistingKeysEnumerated;
return indexNodeValueAddress;
}
hasExistingData = true;
}
collisionAddress = indexNodeValueAddress;
} else if (indexNodeValueAddress < 0) { // indexNodeValueAddress points to duplicates list
collisionAddress = -indexNodeValueAddress;
while (true) {
final int address = myStorage.getInt(collisionAddress);
if (isKeyAtIndex(value, address)) {
if (!saveNewValue) return address;
hasExistingData = true;
break;
}
int newCollisionAddress = myStorage.getInt(collisionAddress + COLLISION_OFFSET);
if (newCollisionAddress == 0) break;
collisionAddress = newCollisionAddress;
}
}
if (onlyCheckForExisting) return NULL_ID;
} else {
if (hasMapping) {
if(!saveNewValue) return indexNodeValueAddress;
hasExistingData = true;
}
}
int newValueId = writeData(value, valueHC);
++myValuesCount;
if (IOStatistics.DEBUG && (myValuesCount & IOStatistics.KEYS_FACTOR_MASK) == 0) {
IOStatistics.dump("Index " +
myFile +
", values " +
myValuesCount +
", existing keys enumerated:"+ myExistingKeysEnumerated +
", storage size:" +
myStorage.length());
myBTree.dumpStatistics();
}
if (collisionAddress != NULL_ID) {
if (hasExistingData) {
if (indexNodeValueAddress > 0) {
myBTree.put(valueHC, newValueId);
} else {
myStorage.putInt(collisionAddress, newValueId);
}
} else {
if (indexNodeValueAddress > 0) {
// organize collision type reference
int duplicatedValueOff = nextDuplicatedValueRecord();
myBTree.put(valueHC, -duplicatedValueOff);
myStorage.putInt(duplicatedValueOff, indexNodeValueAddress); // we will set collision offset in next if
collisionAddress = duplicatedValueOff;
++myCollisions;
}
++myCollisions;
int duplicatedValueOff = nextDuplicatedValueRecord();
myStorage.putInt(collisionAddress + COLLISION_OFFSET, duplicatedValueOff);
myStorage.putInt(duplicatedValueOff, newValueId);
myStorage.putInt(duplicatedValueOff + COLLISION_OFFSET, 0);
}
} else {
myBTree.put(valueHC, newValueId);
}
if (IntToIntBtree.doSanityCheck) {
if (!myInlineKeysNoMapping) {
Data data = valueOf(newValueId);
IntToIntBtree.myAssert(myDataDescriptor.isEqual(value, data));
}
}
return newValueId;
}
catch (IllegalStateException e) {
CorruptedException exception = new CorruptedException(myFile);
exception.initCause(e);
throw exception;
} finally {
unlockStorage();
}
}
@Override
boolean canReEnumerate() {
return true;
}
@Override
public Data valueOf(int idx) throws IOException {
if (myInlineKeysNoMapping) {
assert false:"No valueOf for inline keys with no mapping option";
}
return super.valueOf(idx);
}
private int nextDuplicatedValueRecord() {
assert !myInlineKeysNoMapping;
if (myDuplicatedValuesPageStart == -1 || myDuplicatedValuesPageOffset == myBTree.pageSize) {
myDuplicatedValuesPageStart = allocPage();
myDuplicatedValuesPageOffset = 0;
}
int duplicatedValueOff = myDuplicatedValuesPageOffset;
myDuplicatedValuesPageOffset += COLLISION_OFFSET + 4;
return myDuplicatedValuesPageStart + duplicatedValueOff;
}
@Override
protected void doFlush() throws IOException {
myBTree.doFlush();
storeVars(true);
super.doFlush();
}
private static class RecordBufferHandler extends PersistentEnumeratorBase.RecordBufferHandler<PersistentBTreeEnumerator> {
private byte[] myBuffer;
@Override
int recordWriteOffset(@NotNull PersistentBTreeEnumerator enumerator, @NotNull byte[] buf) {
if (enumerator.myFirstPageStart == -1) {
enumerator.myFirstPageStart = enumerator.myDataPageStart = enumerator.allocPage();
}
if (enumerator.myDataPageOffset + buf.length + 4 > enumerator.myBTree.pageSize) {
assert enumerator.myDataPageOffset + 4 <= enumerator.myBTree.pageSize;
int prevDataPageStart = enumerator.myDataPageStart + enumerator.myBTree.pageSize - 4;
enumerator.myDataPageStart = enumerator.allocPage();
enumerator.myStorage.putInt(prevDataPageStart, enumerator.myDataPageStart);
enumerator.myDataPageOffset = 0;
}
int recordWriteOffset = enumerator.myDataPageOffset;
assert recordWriteOffset + buf.length + 4 <= enumerator.myBTree.pageSize;
enumerator.myDataPageOffset += buf.length;
return recordWriteOffset + enumerator.myDataPageStart;
}
@NotNull
@Override
byte[] getRecordBuffer(@NotNull PersistentBTreeEnumerator enumerator) {
if (myBuffer == null) {
myBuffer = new byte[enumerator.myInlineKeysNoMapping ? 0:RECORD_SIZE];
}
return myBuffer;
}
@Override
void setupRecord(@NotNull PersistentBTreeEnumerator enumerator, int hashCode, int dataOffset, byte[] buf) {
if (!enumerator.myInlineKeysNoMapping) Bits.putInt(buf, 0, dataOffset);
}
}
}