blob: 92009a49fa9e625d297d2dd02366f8f6d0179373 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Google, Inc and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Stefan Xenos (Google) - Initial implementation
*******************************************************************************/
package org.eclipse.jdt.internal.core.nd.field;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.internal.core.nd.ITypeFactory;
import org.eclipse.jdt.internal.core.nd.Nd;
import org.eclipse.jdt.internal.core.nd.db.Database;
import org.eclipse.jdt.internal.core.nd.db.ModificationLog;
import org.eclipse.jdt.internal.core.nd.db.ModificationLog.Tag;
import org.eclipse.jdt.internal.core.nd.util.MathUtils;
/**
* Stores a singly-linked list of blocks, each of which contains a variable number of embedded elements.
* Each block contains a header containing the size of the block and pointer to the next block, followed
* by the embedded elements themselves.
*/
public class FieldList<T> extends BaseField implements IDestructableField {
/**
* Pointer to the first block.
*/
public final static FieldPointer FIRST_BLOCK;
/**
* Pointer to the block where insertions are currently happening. This is only null if there are no allocated
* blocks. If there are any blocks containing elements, this points to the last block with a nonzero number of
* elements.
*/
public final static FieldPointer LAST_BLOCK_WITH_ELEMENTS;
@SuppressWarnings("rawtypes")
private static final StructDef<FieldList> type;
private static final int LIST_HEADER_BYTES;
private static final long MAX_BYTES_IN_A_CHUNK = Database.getBytesThatFitInChunks(1);
private final StructDef<T> elementType;
private final int elementsPerBlock;
private final StructDef<?> ownerType;
private final Tag allocateTag;
private final Tag appendTag;
private final Tag destructTag;
static {
type = StructDef.createAbstract(FieldList.class);
FIRST_BLOCK = type.addPointer();
LAST_BLOCK_WITH_ELEMENTS = type.addPointer();
type.done();
LIST_HEADER_BYTES = MathUtils.roundUpToNearestMultipleOfPowerOfTwo(type.size(), Database.BLOCK_SIZE_DELTA);
}
private static class BlockHeader {
// This points to the next block if there is one, or null if not.
public final static FieldPointer NEXT_BLOCK;
public final static FieldShort BLOCK_SIZE;
public final static FieldShort ELEMENTS_IN_USE;
public static final int BLOCK_HEADER_BYTES;
@SuppressWarnings("hiding")
private static final StructDef<BlockHeader> type;
static {
type = StructDef.createAbstract(BlockHeader.class);
NEXT_BLOCK = type.addPointer();
BLOCK_SIZE = type.addShort();
ELEMENTS_IN_USE = type.addShort();
type.done();
BLOCK_HEADER_BYTES = MathUtils.roundUpToNearestMultipleOfPowerOfTwo(type.size(), Database.BLOCK_SIZE_DELTA);
}
}
private FieldList(StructDef<?> ownerType, StructDef<T> elementType, int elementsPerBlock) {
this.elementType = elementType;
this.elementsPerBlock = elementsPerBlock;
this.ownerType = ownerType;
int fieldNumber = ownerType.getNumFields();
setFieldName("field " + fieldNumber + ", a " + getClass().getSimpleName() //$NON-NLS-1$//$NON-NLS-2$
+ " in struct " + ownerType.getStructName());//$NON-NLS-1$
this.allocateTag = ModificationLog.createTag("Allocating elements for " + getFieldName()); //$NON-NLS-1$
this.appendTag = ModificationLog.createTag("Appending to " + getFieldName()); //$NON-NLS-1$
this.destructTag = ModificationLog.createTag("Deallocating " + getFieldName()); //$NON-NLS-1$
}
/**
* Creates a new {@link FieldList} in the given struct which contains elements of the given type. The resulting list
* will grow by 1 element each time it overflows.
*
* @param ownerStruct
* the struct to which the new list field will be added. Must not have had {@link StructDef#done()}
* invoked on it yet.
* @param elementType
* the type of elements that will be contained in the struct.
* @return a newly-constructed list field in the given struct.
*/
public static <T> FieldList<T> create(StructDef<?> ownerStruct, StructDef<T> elementType) {
return create(ownerStruct, elementType, 1);
}
/**
* Creates a new {@link FieldList} in the given struct which contains elements of the given type. The resulting list
* will grow by the given number of elements each time it overflows.
*
* @param ownerStruct
* the struct to which the new list field will be added. Must not have had {@link StructDef#done()}
* invoked on it yet.
* @param elementType
* the type of elements that will be contained in the struct.
* @param elementsPerBlock
* the number of elements that will be allocated each time the list overflows.
* @return a newly-constructed list field in the given struct.
*/
public static <T> FieldList<T> create(StructDef<?> ownerStruct, StructDef<T> elementType, int elementsPerBlock) {
FieldList<T> result = new FieldList<>(ownerStruct, elementType, elementsPerBlock);
ownerStruct.add(result);
ownerStruct.addDestructableField(result);
return result;
}
private int getElementSize() {
int recordSize = this.elementType.getFactory().getRecordSize();
return MathUtils.roundUpToNearestMultipleOfPowerOfTwo(recordSize, Database.BLOCK_SIZE_DELTA);
}
@Override
public int getRecordSize() {
return LIST_HEADER_BYTES;
}
/**
* Returns the contents of the receiver as a {@link List}.
*
* @param nd the database to be queried.
* @param address the address of the parent struct
*/
public List<T> asList(Nd nd, long address) {
long headerStartAddress = address + this.offset;
long firstBlockAddress = FIRST_BLOCK.get(nd, headerStartAddress);
List<T> result = new ArrayList<>();
long nextBlockAddress = firstBlockAddress;
while (nextBlockAddress != 0) {
long currentBlockAddress = nextBlockAddress;
nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
long firstElementInBlockAddress = currentBlockAddress + BlockHeader.BLOCK_HEADER_BYTES;
readElements(result, nd, firstElementInBlockAddress, elementsInBlock);
}
return result;
}
private void readElements(List<T> result, Nd nd, long nextElementAddress, int count) {
ITypeFactory<T> factory = this.elementType.getFactory();
int size = getElementSize();
for (; count > 0; count--) {
result.add(factory.create(nd, nextElementAddress));
nextElementAddress += size;
}
}
public T append(Nd nd, long address) {
Database db = nd.getDB();
db.getLog().start(this.appendTag);
try {
long headerStartAddress = address + this.offset;
long nextBlockAddress = LAST_BLOCK_WITH_ELEMENTS.get(nd, headerStartAddress);
// Ensure that there's at least one block
long insertionBlockAddress = nextBlockAddress;
if (nextBlockAddress == 0) {
long newBlockAddress = allocateNewBlock(nd, this.elementsPerBlock);
LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, newBlockAddress);
FIRST_BLOCK.put(nd, headerStartAddress, newBlockAddress);
insertionBlockAddress = newBlockAddress;
}
// Check if there's any free space in this block
int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, insertionBlockAddress);
int blockSize = BlockHeader.BLOCK_SIZE.get(nd, insertionBlockAddress);
if (elementsInBlock >= blockSize) {
long nextBlock = BlockHeader.NEXT_BLOCK.get(nd, insertionBlockAddress);
if (nextBlock == 0) {
nextBlock = allocateNewBlock(nd, this.elementsPerBlock);
BlockHeader.NEXT_BLOCK.put(nd, insertionBlockAddress, nextBlock);
}
LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, nextBlock);
insertionBlockAddress = nextBlock;
elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, insertionBlockAddress);
}
BlockHeader.ELEMENTS_IN_USE.put(nd, insertionBlockAddress, (short) (elementsInBlock + 1));
int elementSize = getElementSize();
long resultAddress = insertionBlockAddress + BlockHeader.BLOCK_HEADER_BYTES + elementsInBlock * elementSize;
assert ((resultAddress - Database.BLOCK_HEADER_SIZE) & (Database.BLOCK_SIZE_DELTA - 1)) == 0;
return this.elementType.getFactory().create(nd, resultAddress);
} finally {
db.getLog().end(this.appendTag);
}
}
/**
* Ensures that the receiver will have space for the given number of elements without additional
* allocation. Callers should invoke this prior to a sequence of {@link FieldList#append(Nd, long)}
* calls if they know in advance how many elements will be appended. Will create the minimum number
* of extra blocks needed to the given number of additional elements.
*/
public void allocate(Nd nd, long address, int numElements) {
Database db = nd.getDB();
db.getLog().start(this.allocateTag);
try {
if (numElements == 0) {
// Not an error, but there's nothing to do if the caller didn't actually ask for anything to be allocated.
return;
}
long headerStartAddress = address + this.offset;
long nextBlockAddress = LAST_BLOCK_WITH_ELEMENTS.get(nd, headerStartAddress);
int maxBlockSizeThatFitsInAChunk = (int) ((MAX_BYTES_IN_A_CHUNK - BlockHeader.BLOCK_HEADER_BYTES)
/ getElementSize());
// Ensure that there's at least one block
if (nextBlockAddress == 0) {
int firstAllocation = Math.min(numElements, maxBlockSizeThatFitsInAChunk);
nextBlockAddress = allocateNewBlock(nd, firstAllocation);
LAST_BLOCK_WITH_ELEMENTS.put(nd, headerStartAddress, nextBlockAddress);
FIRST_BLOCK.put(nd, headerStartAddress, nextBlockAddress);
}
// Check if there's any free space in this block
int remainingToAllocate = numElements;
while (true) {
long currentBlockAddress = nextBlockAddress;
nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
int elementsInUse = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
int blockSize = BlockHeader.BLOCK_SIZE.get(nd, currentBlockAddress);
remainingToAllocate -= (blockSize - elementsInUse);
if (remainingToAllocate <= 0) {
break;
}
if (nextBlockAddress == 0) {
nextBlockAddress = allocateNewBlock(nd, Math.min(maxBlockSizeThatFitsInAChunk, numElements));
BlockHeader.NEXT_BLOCK.put(nd, currentBlockAddress, nextBlockAddress);
}
}
} finally {
db.getLog().end(this.allocateTag);
}
}
private long allocateNewBlock(Nd nd, int blockSize) {
short poolId = getMemoryPoolId(nd);
int elementSize = getElementSize();
long bytesNeeded = BlockHeader.BLOCK_HEADER_BYTES + blockSize * elementSize;
// If we're close enough to filling the chunk that we wouldn't be able to fit any more elements anyway, allocate
// the entire chunk. Although it wastes a small amount of space, it ensures that the blocks can be more easily
// reused rather than being fragmented. It also allows freed blocks to be merged via the large block allocator.
if (MAX_BYTES_IN_A_CHUNK - bytesNeeded < elementSize) {
bytesNeeded = MAX_BYTES_IN_A_CHUNK;
}
long result = nd.getDB().malloc(bytesNeeded, poolId);
BlockHeader.BLOCK_SIZE.put(nd, result, (short) blockSize);
return result;
}
private short getMemoryPoolId(Nd nd) {
short poolId = Database.POOL_LINKED_LIST;
if (this.ownerType != null) {
Class<?> structClass = this.ownerType.getStructClass();
if (nd.getTypeRegistry().isRegisteredClass(structClass)) {
poolId = (short) (Database.POOL_FIRST_NODE_TYPE + nd.getNodeType(structClass));
}
}
return poolId;
}
@Override
public void destruct(Nd nd, long address) {
Database db = nd.getDB();
db.getLog().start(this.destructTag);
try {
short poolId = getMemoryPoolId(nd);
long headerStartAddress = address + this.offset;
long firstBlockAddress = FIRST_BLOCK.get(nd, headerStartAddress);
long nextBlockAddress = firstBlockAddress;
while (nextBlockAddress != 0) {
long currentBlockAddress = nextBlockAddress;
nextBlockAddress = BlockHeader.NEXT_BLOCK.get(nd, currentBlockAddress);
int elementsInBlock = BlockHeader.ELEMENTS_IN_USE.get(nd, currentBlockAddress);
destructElements(nd, currentBlockAddress + BlockHeader.BLOCK_HEADER_BYTES, elementsInBlock);
db.free(currentBlockAddress, poolId);
}
db.clearRange(headerStartAddress, getRecordSize());
} finally {
db.getLog().end(this.destructTag);
}
}
private void destructElements(Nd nd, long nextElementAddress, int count) {
ITypeFactory<T> factory = this.elementType.getFactory();
int size = getElementSize();
while (--count >= 0) {
factory.destruct(nd, nextElementAddress);
nextElementAddress += size;
}
}
}