/*
* Copyright 2000-2015 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.openapi.vfs.newvfs.persistent;
import com.intellij.util.io.PagePool;
import com.intellij.util.io.storage.AbstractRecordsTable;
import com.intellij.util.io.storage.RecordIdIterator;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.BitSet;
// Twice as compact as AbstractRecordsTable: 8 bytes per record: int offset ([0..Integer.MAX_INT]), int ((capacity [0..0xFFFF) << 16) | (size [-1..0xFFFF)))
// if int offset is overflowed then new 8 byte record is created to
// hold long offset and original record contains negative new record number,
// same if size / capacity is overflowed: new record is created to hold integer offset / capacity and original record contains its
// negative number
public class CompactRecordsTable extends AbstractRecordsTable {
private final byte[] zeroes;
private final boolean forceSplit;
public CompactRecordsTable(File recordsFile, PagePool pool, boolean forceSplit) throws IOException {
super(recordsFile, pool);
zeroes = new byte[getRecordSize()];
this.forceSplit = forceSplit;
}
@Override
protected int getImplVersion() {
return 1;
}
@Override
protected int getRecordSize() {
return 8;
}
@Override
protected byte[] getZeros() {
return zeroes;
}
private static final int ADDRESS_OFFSET = 0;
private static final int SIZE_AND_CAPACITY_OFFSET = 4;
private static final int SIZE_OFFSET_IN_INDIRECT_RECORD = 0;
private static final int CAPACITY_OFFSET_IN_INDIRECT_RECORD = 4;
@Override
public long getAddress(int record) {
int address = myStorage.getInt(getOffset(record, ADDRESS_OFFSET));
if (address < 0) { // read address from indirect record
return super.getAddress(-address);
}
return address;
}
@Override
public void setAddress(int record, long address) {
markDirty();
int addressOfRecordAbsoluteOffset = getOffset(record, ADDRESS_OFFSET);
int existing_address = myStorage.getInt(addressOfRecordAbsoluteOffset);
if (existing_address < 0) { // update address in indirect record
super.setAddress(-existing_address, address);
return;
}
if (address > Integer.MAX_VALUE || address < 0 || forceSplit) {
// nonnegative integer address is not enough and we introduce indirect record
int extendedRecord = doCreateNewRecord();
super.setAddress(extendedRecord, address);
myStorage.putInt(addressOfRecordAbsoluteOffset, -extendedRecord);
}
else {
myStorage.putInt(addressOfRecordAbsoluteOffset, (int)address);
}
}
private int doCreateNewRecord() {
try {
return createNewRecord();
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private static final int SIZE_MASK = 0xFFFF;
private static final int CAPACITY_MASK = 0x7FFF0000;
private static final int CAPACITY_SHIFT = 16;
private static final int SPECIAL_POSITIVE_VALUE_FOR_SPECIAL_NEGATIVE_SIZE = 0xFFFF;
private static final int SPECIAL_NEGATIVE_SIZE = -1;
@Override
public int getSize(int record) {
int currentValue = myStorage.getInt(getOffset(record, SIZE_AND_CAPACITY_OFFSET));
if (currentValue < 0) {
// read size from indirect record
return myStorage.getInt(getOffset(-currentValue, SIZE_OFFSET_IN_INDIRECT_RECORD));
}
int i = currentValue & SIZE_MASK;
if (i == SPECIAL_POSITIVE_VALUE_FOR_SPECIAL_NEGATIVE_SIZE) i = SPECIAL_NEGATIVE_SIZE;
return i;
}
@Override
public void setSize(int record, int size) {
markDirty();
int sizeAndCapacityOfRecordAbsoluteOffset = getOffset(record, SIZE_AND_CAPACITY_OFFSET);
int currentValue = myStorage.getInt(sizeAndCapacityOfRecordAbsoluteOffset);
if (currentValue < 0) {
// update size in indirect record
myStorage.putInt(getOffset(-currentValue, SIZE_OFFSET_IN_INDIRECT_RECORD), size);
return;
}
// size to fit in normal record [-1 .. 0xFFFF)
if (size >= 0xFFFF || size < SPECIAL_NEGATIVE_SIZE || forceSplit) {
// introduce indirect record able to hold larger size range
extendSizeAndCapacityRecord(record, size, getCapacity(record));
return;
}
if (size == SPECIAL_NEGATIVE_SIZE) {
size = SPECIAL_POSITIVE_VALUE_FOR_SPECIAL_NEGATIVE_SIZE;
}
myStorage.putInt(sizeAndCapacityOfRecordAbsoluteOffset, size | (currentValue & CAPACITY_MASK));
}
private void extendSizeAndCapacityRecord(int record, int size, int capacity) {
int extendedRecord = doCreateNewRecord();
myStorage.putInt(getOffset(extendedRecord, SIZE_OFFSET_IN_INDIRECT_RECORD), size);
myStorage.putInt(getOffset(extendedRecord, CAPACITY_OFFSET_IN_INDIRECT_RECORD), capacity);
myStorage.putInt(getOffset(record, SIZE_AND_CAPACITY_OFFSET), -extendedRecord);
}
@Override
public int getCapacity(int record) {
int currentValue = myStorage.getInt(getOffset(record, SIZE_AND_CAPACITY_OFFSET));
if (currentValue < 0) {
// read capacity from indirect record
return myStorage.getInt(getOffset(-currentValue, CAPACITY_OFFSET_IN_INDIRECT_RECORD));
}
return (currentValue & CAPACITY_MASK) >> CAPACITY_SHIFT;
}
@Override
public void setCapacity(int record, int capacity) {
markDirty();
int sizeAndCapacityOfRecordAbsoluteOffset = getOffset(record, SIZE_AND_CAPACITY_OFFSET);
int currentValue = myStorage.getInt(sizeAndCapacityOfRecordAbsoluteOffset);
if (currentValue < 0) {
// update capacity in indirect record
myStorage.putInt(getOffset(-currentValue, CAPACITY_OFFSET_IN_INDIRECT_RECORD), capacity);
return;
}
// size to fit in normal record [0 .. 0x7FFF]
if (capacity > 0x7fff || capacity < 0 || forceSplit) {
// introduce indirect record able to hold larger capacity range
extendSizeAndCapacityRecord(record, getSize(record), capacity);
return;
}
myStorage.putInt(sizeAndCapacityOfRecordAbsoluteOffset, (currentValue & SIZE_MASK) | (capacity << CAPACITY_SHIFT));
}
@Override
public void deleteRecord(int record) throws IOException {
final int sizeAndCapacityOfRecordAbsoluteOffset = getOffset(record, SIZE_AND_CAPACITY_OFFSET);
final int sizeAndCapacityValue = myStorage.getInt(sizeAndCapacityOfRecordAbsoluteOffset);
final int addressOfRecordAbsoluteOffset = getOffset(record, ADDRESS_OFFSET);
final int existingAddressValue = myStorage.getInt(addressOfRecordAbsoluteOffset);
super.deleteRecord(record);
if (sizeAndCapacityValue < 0) {
super.deleteRecord(-sizeAndCapacityValue);
}
if (existingAddressValue < 0) {
super.deleteRecord(-existingAddressValue);
}
}
@Override
public RecordIdIterator createRecordIdIterator() throws IOException {
final BitSet extraRecordsIds = buildIdSetOfExtraRecords();
final RecordIdIterator iterator = super.createRecordIdIterator();
return new RecordIdIterator() {
int nextId = scanToNextId();
private int scanToNextId() {
while(iterator.hasNextId()) {
int next = iterator.nextId();
if ( !extraRecordsIds.get(next)) return next;
}
return -1;
}
@Override
public boolean hasNextId() {
return nextId != -1;
}
@Override
public int nextId() {
assert hasNextId();
int result = nextId;
nextId = scanToNextId();
return result;
}
@Override
public boolean validId() {
assert hasNextId();
return getSize(nextId) != -1;
}
};
}
@NotNull
private BitSet buildIdSetOfExtraRecords() throws IOException {
final BitSet extraRecords = new BitSet();
final RecordIdIterator iterator = super.createRecordIdIterator();
while(iterator.hasNextId()) {
int recordId = iterator.nextId();
final int sizeAndCapacityOfRecordAbsoluteOffset = getOffset(recordId, SIZE_AND_CAPACITY_OFFSET);
final int sizeAndCapacityValue = myStorage.getInt(sizeAndCapacityOfRecordAbsoluteOffset);
final int addressOfRecordAbsoluteOffset = getOffset(recordId, ADDRESS_OFFSET);
final int existingAddressValue = myStorage.getInt(addressOfRecordAbsoluteOffset);
if (sizeAndCapacityValue < 0) {
extraRecords.set(-sizeAndCapacityValue);
}
if (existingAddressValue < 0) {
extraRecords.set(-existingAddressValue);
}
}
return extraRecords;
}
}