blob: 991f9a6770fe559389727184092cd277fd95c2a3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013, 2017 Ericsson
*
* All rights reserved. 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:
* Marc-Andre Laperle - Initial API and implementation
*******************************************************************************/
package org.eclipse.tracecompass.internal.tmf.core.trace.indexer;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfPersistentlyIndexable;
import org.eclipse.tracecompass.tmf.core.trace.indexer.checkpoint.ITmfCheckpoint;
/**
* A BTree made of BTreeNodes representing a series of ITmfCheckpoints ordered
* by time stamps. {@link BTreeNodeCache } is used to improve performance by
* caching some nodes in memory and the other nodes are kept on disk.
*
* @author Marc-Andre Laperle
*/
public class BTree extends AbstractFileCheckpointCollection {
/**
* Typical BTree file name
*/
public static final String INDEX_FILE_NAME = "checkpoint_btree.idx"; //$NON-NLS-1$
private static final int SUB_VERSION = 4;
private static final boolean ALWAYS_CACHE_ROOT = true;
private final int fMaxNumEntries;
private final int fMaxNumChildren;
private final int fMedianEntry;
private BTreeHeader fBTreeHeader;
// Cached values
private int nodeSize = -1;
private final ByteBuffer fNodeByteBuffer;
private final BTreeNodeCache fNodeCache;
private class BTreeHeader extends CheckpointCollectionFileHeader {
private static final int SIZE = LONG_SIZE + INT_SIZE;
private long fRoot;
private final int fSubVersion;
private BTreeHeader(int version, int subVersion) {
super(version);
fSubVersion = subVersion;
}
private BTreeHeader(RandomAccessFile randomAccessFile) throws IOException {
super(randomAccessFile);
fRoot = randomAccessFile.readLong();
fSubVersion = randomAccessFile.readInt();
}
@Override
public int getSubVersion() {
return fSubVersion;
}
@Override
public int getSize() {
return SIZE + super.getSize();
}
@Override
public void serialize(RandomAccessFile randomAccessFile) throws IOException {
super.serialize(randomAccessFile);
randomAccessFile.writeLong(fRoot);
randomAccessFile.writeInt(fSubVersion);
}
}
@Override
protected CheckpointCollectionFileHeader createHeader() {
fBTreeHeader = new BTreeHeader(getVersion(), SUB_VERSION);
return fBTreeHeader;
}
@Override
protected CheckpointCollectionFileHeader createHeader(RandomAccessFile randomAccessFile) throws IOException {
fBTreeHeader = new BTreeHeader(randomAccessFile);
return fBTreeHeader;
}
@Override
protected int getSubVersion() {
return SUB_VERSION;
}
/**
* Constructs a BTree for a given trace from scratch or from an existing
* file. The degree is used to calibrate the number of entries in each node
* which can affect performance. When the BTree is created from scratch, it
* is populated by subsequent calls to {@link #insert}.
*
* @param degree
* the degree to use in the tree
* @param file
* the file to use as the persistent storage
* @param trace
* the trace
*/
public BTree(int degree, File file, ITmfPersistentlyIndexable trace) {
super(file, trace);
fMaxNumEntries = 2 * degree - 1;
fMaxNumChildren = 2 * degree;
fMedianEntry = degree - 1;
fNodeByteBuffer = ByteBuffer.allocate(getNodeSize());
fNodeByteBuffer.clear();
fNodeCache = new BTreeNodeCache(this);
BTreeNode rootNode = isCreatedFromScratch() ? allocateNode() : fNodeCache.getNode(fBTreeHeader.fRoot);
setRootNode(rootNode);
}
/**
* Insert a checkpoint into the file-backed BTree
*
* @param checkpoint
* the checkpoint to insert
*/
@Override
public void insert(ITmfCheckpoint checkpoint) {
markDirty();
insert(checkpoint, fBTreeHeader.fRoot, null, 0);
}
private void setRootNode(BTreeNode newRootNode) {
fBTreeHeader.fRoot = newRootNode.getOffset();
if (ALWAYS_CACHE_ROOT) {
fNodeCache.setRootNode(newRootNode);
} else {
fNodeCache.addNode(newRootNode);
}
}
private void insert(ITmfCheckpoint checkpoint, long nodeOffset, BTreeNode pParent, int iParent) {
BTreeNode parent = pParent;
BTreeNode node = fNodeCache.getNode(nodeOffset);
// If this node is full (last entry isn't null), split it
if (node.getEntry(fMaxNumEntries - 1) != null) {
ITmfCheckpoint median = node.getEntry(fMedianEntry);
if (median.compareTo(checkpoint) == 0) {
// Found it
return;
}
// Split it.
// Create the new node and move the larger entries over.
BTreeNode newnode = allocateNode();
fNodeCache.addNode(newnode);
long newNodeOffset = newnode.getOffset();
for (int i = 0; i < fMedianEntry; ++i) {
newnode.setEntry(i, node.getEntry(fMedianEntry + 1 + i));
node.setEntry(fMedianEntry + 1 + i, null);
newnode.setChild(i, node.getChild(fMedianEntry + 1 + i));
node.setChild(fMedianEntry + 1 + i, BTreeNode.NULL_CHILD);
}
newnode.setChild(fMedianEntry, node.getChild(fMaxNumEntries));
node.setChild(fMaxNumEntries, BTreeNode.NULL_CHILD);
if (parent == null) {
parent = allocateNode();
setRootNode(parent);
parent.setChild(0, nodeOffset);
} else {
// Insert the median into the parent.
for (int i = fMaxNumEntries - 2; i >= iParent; --i) {
ITmfCheckpoint r = parent.getEntry(i);
if (r != null) {
parent.setEntry(i + 1, r);
parent.setChild(i + 2, parent.getChild(i + 1));
}
}
}
fNodeCache.getNode(parent.getOffset());
parent.setEntry(iParent, median);
parent.setChild(iParent + 1, newNodeOffset);
node.setEntry(fMedianEntry, null);
// Set the node to the correct one to follow.
if (checkpoint.compareTo(median) > 0) {
node = newnode;
}
}
// Binary search to find the insert point.
int lower = 0;
int upper = fMaxNumEntries - 1;
while (lower < upper && node.getEntry(upper - 1) == null) {
upper--;
}
while (lower < upper) {
int middle = (lower + upper) / 2;
ITmfCheckpoint check = node.getEntry(middle);
if (check == null) {
upper = middle;
} else {
int compare = check.compareTo(checkpoint);
if (compare > 0) {
upper = middle;
} else if (compare < 0) {
lower = middle + 1;
} else {
// Found it, no insert
return;
}
}
}
final int i = lower;
long child = node.getChild(i);
if (child != BTreeNode.NULL_CHILD) {
// Visit the children.
insert(checkpoint, child, node, i);
} else {
// We are at the leaf, add us in.
// First copy everything after over one.
for (int j = fMaxNumEntries - 2; j >= i; --j) {
ITmfCheckpoint r = node.getEntry(j);
if (r != null) {
node.setEntry(j + 1, r);
}
}
node.setEntry(i, checkpoint);
CheckpointCollectionFileHeader header = getHeader();
++header.fSize;
return;
}
}
int getNodeSize() {
if (nodeSize == -1) {
nodeSize = INT_SIZE; // num entries
nodeSize += getTrace().getCheckpointSize() * fMaxNumEntries;
nodeSize += LONG_SIZE * fMaxNumChildren;
}
return nodeSize;
}
private BTreeNode allocateNode() {
try {
long offset = getRandomAccessFile().length();
getRandomAccessFile().setLength(offset + getNodeSize());
BTreeNode node = new BTreeNode(this, offset);
return node;
} catch (IOException e) {
Activator.logError(MessageFormat.format(Messages.BTree_IOErrorAllocatingNode, getFile()), e);
}
return null;
}
@Override
public long binarySearch(ITmfCheckpoint checkpoint) {
BTreeCheckpointVisitor v = new BTreeCheckpointVisitor(checkpoint);
accept(v);
return v.getCheckpointRank();
}
/**
* Accept a visitor. This visitor is used to search through the whole tree.
*
* @param treeVisitor
* the visitor to accept
*/
public void accept(IBTreeVisitor treeVisitor) {
accept(fBTreeHeader.fRoot, treeVisitor);
}
private void accept(long nodeOffset, IBTreeVisitor visitor) {
if (nodeOffset == BTreeNode.NULL_CHILD || getRandomAccessFile() == null) {
return;
}
BTreeNode node = fNodeCache.getNode(nodeOffset);
// Binary search to find first entry greater or equal.
int lower = 0;
int upper = fMaxNumEntries - 1;
while (lower < upper && node.getEntry(upper - 1) == null) {
upper--;
}
while (lower < upper) {
int middle = (lower + upper) / 2;
ITmfCheckpoint middleCheckpoint = node.getEntry(middle);
if (middleCheckpoint == null) {
upper = middle;
} else {
int compare = visitor.compare(middleCheckpoint);
if (compare == 0) {
return;
} else if (compare > 0) {
upper = middle;
} else {
lower = middle + 1;
}
}
}
// Start with first record greater or equal, reuse comparison
// results.
int i = lower;
for (; i < fMaxNumEntries; ++i) {
ITmfCheckpoint record = node.getEntry(i);
if (record == null) {
break;
}
int compare = visitor.compare(record);
if (compare > 0) {
// Start point is to the left.
accept(node.getChild(i), visitor);
return;
} else if (compare == 0) {
return;
}
}
accept(node.getChild(i), visitor);
return;
}
/**
* Get the maximum number of entries in a node
*
* @return the maximum number of entries in a node
*/
int getMaxNumEntries() {
return fMaxNumEntries;
}
/**
* Get the maximum number of children in a node
*
* @return the maximum number of children in a node
*/
int getMaxNumChildren() {
return fMaxNumChildren;
}
ByteBuffer getNodeByteBuffer() {
return fNodeByteBuffer;
}
@Override
public void dispose() {
if (fNodeCache != null && getRandomAccessFile() != null) {
fNodeCache.serialize();
}
super.dispose();
}
}