| /******************************************************************************* |
| * 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(); |
| } |
| } |