| /******************************************************************************* |
| * Copyright (c) 2017 École Polytechnique de Montréal |
| * |
| * 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 |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.provisional.datastore.core.historytree; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.nio.channels.ClosedChannelException; |
| import java.nio.channels.FileChannel; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Deque; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import java.util.function.Predicate; |
| |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.tracecompass.common.core.NonNullUtils; |
| import org.eclipse.tracecompass.datastore.core.interval.IHTInterval; |
| import org.eclipse.tracecompass.datastore.core.interval.IHTIntervalReader; |
| import org.eclipse.tracecompass.internal.datastore.core.historytree.HtIo; |
| import org.eclipse.tracecompass.internal.provisional.datastore.core.condition.TimeRangeCondition; |
| import org.eclipse.tracecompass.internal.provisional.datastore.core.exceptions.RangeException; |
| import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.IHTNode.NodeType; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| |
| /** |
| * Base class for history trees that encapsulates the logic to read from/write |
| * to a file. |
| * |
| * @author Alexandre Montplaisir |
| * @author Geneviève Bastien |
| * @param <E> |
| * The type of intervals that will be saved in the tree |
| * @param <N> |
| * The base type of the nodes of this tree |
| */ |
| public abstract class AbstractHistoryTree<E extends IHTInterval, N extends HTNode<E>> |
| implements IHistoryTree<E> { |
| |
| /** |
| * Interface for history to create the various HTNodes |
| * |
| * @param <E> |
| * The type of intervals that will be saved in the node |
| * @param <N> |
| * The base type of the nodes of this tree |
| */ |
| @FunctionalInterface |
| public interface IHTNodeFactory<E extends IHTInterval, N extends HTNode<E>> { |
| |
| /** |
| * Creates a new node for the specific history tree |
| * |
| * @param type |
| * The type of node to create. See {@link IHTNode.NodeType}. |
| * @param blockSize |
| * The size (in bytes) of each node once serialized to disk |
| * @param maxChildren |
| * The maximum number of amount a single core node can have |
| * @param seqNumber |
| * The (unique) sequence number assigned to this particular |
| * node |
| * @param parentSeqNumber |
| * The sequence number of this node's parent node |
| * @param start |
| * The earliest timestamp stored in this node |
| * @return The new core node |
| */ |
| N createNode(NodeType type, int blockSize, int maxChildren, |
| int seqNumber, int parentSeqNumber, long start); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Tree-specific configuration |
| // ------------------------------------------------------------------------ |
| |
| /* Tree configuration constants */ |
| private final File fHistoryFile; |
| private final int fBlockSize; |
| private final int fMaxChildren; |
| private final int fProviderVersion; |
| private final long fTreeStart; |
| private final IHTIntervalReader<E> fIntervalReader; |
| |
| /** Reader/writer object */ |
| private HtIo<E, N> fTreeIO; |
| |
| // ------------------------------------------------------------------------ |
| // Variable Fields (will change throughout the existence of the SHT) |
| // ------------------------------------------------------------------------ |
| |
| /** Latest timestamp found in the tree (at any given moment) */ |
| private long fTreeEnd; |
| |
| /** The total number of nodes that exists in this tree */ |
| private int fNodeCount; |
| |
| /** "Cache" to keep the active nodes in memory */ |
| private final List<N> fLatestBranch; |
| |
| /* Lock used to protect the accesses to the HT_IO object */ |
| private final ReentrantReadWriteLock fRwl = new ReentrantReadWriteLock(false); |
| |
| private transient @Nullable List<N> fLatestBranchSnapshot = null; |
| |
| /** |
| * Create a new State History from scratch, specifying all configuration |
| * parameters. |
| * |
| * @param stateHistoryFile |
| * The name of the history file |
| * @param blockSize |
| * The size of each "block" on disk in bytes. One node will |
| * always fit in one block. It should be at least 4096. |
| * @param maxChildren |
| * The maximum number of children allowed per core (non-leaf) |
| * node. |
| * @param providerVersion |
| * The version of the state provider. If a file already exists, |
| * and their versions match, the history file will not be rebuilt |
| * uselessly. |
| * @param treeStart |
| * The start time of the history |
| * @param intervalReader |
| * The factory to create new tree intervals when reading from |
| * the disk |
| * @throws IOException |
| * If an error happens trying to open/write to the file |
| * specified in the config |
| */ |
| public AbstractHistoryTree(File stateHistoryFile, |
| int blockSize, |
| int maxChildren, |
| int providerVersion, |
| long treeStart, |
| IHTIntervalReader<E> intervalReader) throws IOException { |
| /* |
| * Simple check to make sure we have enough place in the 0th block for |
| * the tree configuration |
| */ |
| if (blockSize < TREE_HEADER_SIZE) { |
| throw new IllegalArgumentException(); |
| } |
| |
| fHistoryFile = stateHistoryFile; |
| fBlockSize = blockSize; |
| fMaxChildren = maxChildren; |
| fProviderVersion = providerVersion; |
| fTreeStart = treeStart; |
| fIntervalReader = intervalReader; |
| |
| fTreeEnd = treeStart; |
| fNodeCount = 0; |
| fLatestBranch = NonNullUtils.checkNotNull(Collections.synchronizedList(new ArrayList<>())); |
| |
| /* Prepare the IO object */ |
| fTreeIO = new HtIo<>(stateHistoryFile, |
| blockSize, |
| maxChildren, |
| true, |
| intervalReader, |
| getNodeFactory()); |
| |
| /* Add the first node to the tree */ |
| N firstNode = initNewLeafNode(-1, treeStart); |
| fLatestBranch.add(firstNode); |
| } |
| |
| /** |
| * "Reader" constructor : instantiate a SHTree from an existing tree file on |
| * disk |
| * |
| * @param existingStateFile |
| * Path/filename of the history-file we are to open |
| * @param expectedProviderVersion |
| * The expected version of the state provider |
| * @param intervalReader |
| * The factory used to read segments from the history tree |
| * @throws IOException |
| * If an error happens reading the file |
| */ |
| public AbstractHistoryTree(File existingStateFile, |
| int expectedProviderVersion, |
| IHTIntervalReader<E> intervalReader) throws IOException { |
| /* |
| * Open the file ourselves, get the tree header information we need, |
| * then pass on the descriptor to the TreeIO object. |
| */ |
| int rootNodeSeqNb, res; |
| int bs, maxc; |
| long startTime; |
| |
| /* Java I/O mumbo jumbo... */ |
| if (!existingStateFile.exists()) { |
| throw new IOException("Selected state file does not exist"); //$NON-NLS-1$ |
| } |
| if (existingStateFile.length() <= 0) { |
| throw new IOException("Empty target file"); //$NON-NLS-1$ |
| } |
| |
| try (FileInputStream fis = new FileInputStream(existingStateFile); |
| FileChannel fc = fis.getChannel();) { |
| |
| ByteBuffer buffer = ByteBuffer.allocate(TREE_HEADER_SIZE); |
| buffer.order(ByteOrder.LITTLE_ENDIAN); |
| buffer.clear(); |
| |
| res = fc.read(buffer); |
| if (res != TREE_HEADER_SIZE) { |
| throw new IOException("Invalid header size"); //$NON-NLS-1$ |
| } |
| |
| buffer.flip(); |
| |
| /* |
| * Check the magic number to make sure we're opening the right type |
| * of file |
| */ |
| res = buffer.getInt(); |
| if (res != getMagicNumber()) { |
| throw new IOException("Wrong magic number"); //$NON-NLS-1$ |
| } |
| |
| res = buffer.getInt(); /* File format version number */ |
| if (res != getFileVersion()) { |
| throw new IOException("Mismatching History Tree file format versions"); //$NON-NLS-1$ |
| } |
| |
| res = buffer.getInt(); /* Event handler's version number */ |
| if (res != expectedProviderVersion) { |
| /* |
| * The existing history was built using an event handler that |
| * doesn't match the current one in the framework. |
| * |
| * Information could be all wrong. Instead of keeping an |
| * incorrect history file, a rebuild is done. |
| */ |
| throw new IOException("Mismatching event handler versions"); //$NON-NLS-1$ |
| } |
| |
| bs = buffer.getInt(); /* Block Size */ |
| maxc = buffer.getInt(); /* Max nb of children per node */ |
| |
| fNodeCount = buffer.getInt(); |
| rootNodeSeqNb = buffer.getInt(); |
| startTime = buffer.getLong(); |
| |
| /* Set all other permanent configuration options */ |
| fHistoryFile = existingStateFile; |
| fBlockSize = bs; |
| fMaxChildren = maxc; |
| fProviderVersion = expectedProviderVersion; |
| fIntervalReader = intervalReader; |
| fTreeStart = startTime; |
| } |
| |
| /* |
| * FIXME We close fis here and the TreeIO will then reopen the same |
| * file, not extremely elegant. But how to pass the information here to |
| * the SHT otherwise? |
| */ |
| fTreeIO = new HtIo<>(fHistoryFile, |
| fBlockSize, |
| fMaxChildren, |
| false, |
| fIntervalReader, |
| getNodeFactory()); |
| |
| fLatestBranch = buildLatestBranch(rootNodeSeqNb); |
| fTreeEnd = getRootNode().getNodeEnd(); |
| |
| /* |
| * Make sure the history start time we read previously is consistent |
| * with was is actually in the root node. |
| */ |
| if (startTime != getRootNode().getNodeStart()) { |
| throw new IOException("Inconsistent start times in the " + //$NON-NLS-1$ |
| "history file, it might be corrupted."); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Rebuild the latestBranch "cache" object by reading the nodes from disk |
| * (When we are opening an existing file on disk and want to append to it, |
| * for example). |
| * |
| * @param rootNodeSeqNb |
| * The sequence number of the root node, so we know where to |
| * start |
| * @throws ClosedChannelException |
| */ |
| private List<N> buildLatestBranch(int rootNodeSeqNb) throws ClosedChannelException { |
| List<N> list = new ArrayList<>(); |
| |
| N nextChildNode = fTreeIO.readNode(rootNodeSeqNb); |
| list.add(nextChildNode); |
| |
| // TODO: Do we need the full latest branch? The latest leaf may not be |
| // the one we'll query first... Won't it build itself later? |
| |
| /* Follow the last branch up to the leaf */ |
| while (nextChildNode.getNodeType() == HTNode.NodeType.CORE) { |
| nextChildNode = fTreeIO.readNode(nextChildNode.getLatestChild()); |
| list.add(nextChildNode); |
| } |
| return Collections.synchronizedList(list); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Accessors |
| // ------------------------------------------------------------------------ |
| |
| @Override |
| public long getTreeStart() { |
| return fTreeStart; |
| } |
| |
| @Override |
| public long getTreeEnd() { |
| return fTreeEnd; |
| } |
| |
| /** |
| * Get the number of nodes in this tree. |
| * |
| * @return The number of nodes |
| */ |
| public int getNodeCount() { |
| return fNodeCount; |
| } |
| |
| /** |
| * Get the current root node of this tree |
| * |
| * @return The root node |
| */ |
| public N getRootNode() { |
| return fLatestBranch.get(0); |
| } |
| |
| @Override |
| public long getFileSize() { |
| return fHistoryFile.length(); |
| } |
| |
| /** |
| * Return the latest branch of the tree. That branch is immutable. |
| * |
| * @return The immutable latest branch |
| */ |
| @VisibleForTesting |
| protected final List<N> getLatestBranch() { |
| List<N> latestBranchSnapshot = fLatestBranchSnapshot; |
| if (latestBranchSnapshot == null) { |
| synchronized (fLatestBranch) { |
| latestBranchSnapshot = ImmutableList.copyOf(fLatestBranch); |
| fLatestBranchSnapshot = latestBranchSnapshot; |
| } |
| } |
| return latestBranchSnapshot; |
| } |
| |
| /** |
| * Get the node in the latest branch at a depth. If the depth is too large, |
| * it will throw an IndexOutOfBoundsException |
| * |
| * @param depth |
| * The depth at which to get the node |
| * @return The node at depth |
| */ |
| protected N getLatestNode(int depth) { |
| if (depth > fLatestBranch.size()) { |
| throw new IndexOutOfBoundsException("Trying to get latest node too deep"); //$NON-NLS-1$ |
| } |
| return fLatestBranch.get(depth); |
| } |
| |
| /** |
| * Get the magic number for the history file. This number should be specific |
| * for each implementation of the history tree. |
| * |
| * @return The magic number for the history file |
| */ |
| protected abstract int getMagicNumber(); |
| |
| /** |
| * Get the file version for the history file. This file version should be |
| * modified for a history tree class whenever changing the format of the |
| * file. Different versions of the file may not be compatible. |
| * |
| * @return The file version for the history file. |
| */ |
| protected abstract int getFileVersion(); |
| |
| /** |
| * Get the factory to use to create new nodes for this history tree. |
| * |
| * This method is called in the constructor of the abstract class, so |
| * assigning the factory to a final field may cause NullPointerException |
| * since that final field may not be initialized the first time this is |
| * called. |
| * |
| * @return The NodeFactory for the History Tree |
| */ |
| protected abstract IHTNodeFactory<E, N> getNodeFactory(); |
| |
| /** |
| * Read a node with a given sequence number |
| * |
| * @param seqNum |
| * The sequence number of the node to read |
| * @return The HTNode object |
| * @throws ClosedChannelException |
| * Exception thrown when reading the node, if the file was |
| * closed |
| */ |
| @VisibleForTesting |
| protected @NonNull N getNode(int seqNum) throws ClosedChannelException { |
| // First, check in the latest branch if the node is there |
| for (N node : fLatestBranch) { |
| if (node.getSequenceNumber() == seqNum) { |
| return node; |
| } |
| } |
| return fTreeIO.readNode(seqNum); |
| } |
| |
| /** |
| * Retrieve the TreeIO object. Should only be used for testing. |
| * |
| * @return The TreeIO |
| */ |
| @VisibleForTesting |
| HtIo<E, N> getTreeIO() { |
| return fTreeIO; |
| } |
| |
| // ------------------------------------------------------------------------ |
| // HT_IO interface |
| // ------------------------------------------------------------------------ |
| |
| // TODO Remove from here |
| @Override |
| public FileInputStream supplyATReader() { |
| fRwl.readLock().lock(); |
| try { |
| return fTreeIO.supplyATReader(getNodeCount()); |
| } finally { |
| fRwl.readLock().unlock(); |
| } |
| } |
| |
| // TODO Remove from here |
| @Override |
| public File supplyATWriterFile() { |
| return fHistoryFile; |
| } |
| |
| // TODO Remove from here |
| @Override |
| public long supplyATWriterFilePos() { |
| return IHistoryTree.TREE_HEADER_SIZE |
| + ((long) getNodeCount() * fBlockSize); |
| } |
| |
| /** |
| * Read a node from the tree. |
| * |
| * @param seqNumber |
| * The sequence number of the node to read |
| * @return The node |
| * @throws ClosedChannelException |
| * If the tree IO is unavailable |
| */ |
| public N readNode(int seqNumber) throws ClosedChannelException { |
| /* Try to read the node from memory */ |
| synchronized (fLatestBranch) { |
| for (N node : fLatestBranch) { |
| if (node.getSequenceNumber() == seqNumber) { |
| return node; |
| } |
| } |
| } |
| |
| fRwl.readLock().lock(); |
| try { |
| /* Read the node from disk */ |
| return fTreeIO.readNode(seqNumber); |
| } finally { |
| fRwl.readLock().unlock(); |
| } |
| } |
| |
| /** |
| * Write a node object to the history file. |
| * |
| * @param node |
| * The node to write to disk |
| */ |
| public void writeNode(N node) { |
| fRwl.readLock().lock(); |
| try { |
| fTreeIO.writeNode(node); |
| } finally { |
| fRwl.readLock().unlock(); |
| } |
| } |
| |
| /** |
| * Close the history file. |
| */ |
| @Override |
| public void closeFile() { |
| fRwl.writeLock().lock(); |
| try { |
| fTreeIO.closeFile(); |
| clearContent(); |
| } finally { |
| fRwl.writeLock().unlock(); |
| } |
| } |
| |
| /** |
| * Delete the history file. |
| */ |
| @Override |
| public void deleteFile() { |
| fRwl.writeLock().lock(); |
| try { |
| fTreeIO.deleteFile(); |
| clearContent(); |
| } finally { |
| fRwl.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void cleanFile() throws IOException { |
| fRwl.writeLock().lock(); |
| try { |
| closeTree(fTreeEnd); |
| fTreeIO.deleteFile(); |
| |
| fTreeIO = new HtIo<>(fHistoryFile, |
| fBlockSize, |
| fMaxChildren, |
| true, |
| fIntervalReader, |
| getNodeFactory()); |
| |
| clearContent(); |
| /* Add the first node to the tree */ |
| N firstNode = initNewLeafNode(-1, fTreeStart); |
| fLatestBranch.add(firstNode); |
| } finally { |
| fRwl.writeLock().unlock(); |
| } |
| } |
| |
| private void clearContent() { |
| // Re-initialize the content of the tree after the file is deleted or |
| // closed |
| fNodeCount = 0; |
| fLatestBranch.clear(); |
| } |
| |
| // ------------------------------------------------------------------------ |
| // Operations |
| // ------------------------------------------------------------------------ |
| |
| /** |
| * Insert an interval in the tree. |
| * |
| * @param interval |
| * The interval to be inserted |
| * @throws RangeException |
| * If the start of end time of the interval are invalid |
| */ |
| @Override |
| public synchronized void insert(E interval) throws RangeException { |
| if (interval.getStart() < fTreeStart) { |
| throw new RangeException("Interval Start:" + interval.getStart() + ", Config Start:" + fTreeStart); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| tryInsertAtNode(interval, fLatestBranch.size() - 1); |
| } |
| |
| /** |
| * Add a new empty core node to the tree. |
| * |
| * @param parentSeqNumber |
| * Sequence number of this node's parent |
| * @param startTime |
| * Start time of the new node |
| * @return The newly created node |
| */ |
| protected final N initNewCoreNode(int parentSeqNumber, long startTime) { |
| N newNode = getNodeFactory().createNode(NodeType.CORE, fBlockSize, fMaxChildren, |
| fNodeCount, parentSeqNumber, startTime); |
| fNodeCount++; |
| return newNode; |
| } |
| |
| /** |
| * Add a new empty leaf node to the tree. |
| * |
| * @param parentSeqNumber |
| * Sequence number of this node's parent |
| * @param startTime |
| * Start time of the new node |
| * @return The newly created node |
| */ |
| protected final N initNewLeafNode(int parentSeqNumber, long startTime) { |
| N newNode = getNodeFactory().createNode(NodeType.LEAF, fBlockSize, fMaxChildren, |
| fNodeCount, parentSeqNumber, startTime); |
| fNodeCount++; |
| return newNode; |
| } |
| |
| /** |
| * Inner method to find in which node we should add the interval. |
| * |
| * @param interval |
| * The interval to add to the tree |
| * @param depth |
| * The index *in the latestBranch* where we are trying the |
| * insertion |
| */ |
| protected final void tryInsertAtNode(E interval, int depth) { |
| N targetNode = fLatestBranch.get(depth); |
| informInsertingAtDepth(depth); |
| |
| /* Verify if there is enough room in this node to store this interval */ |
| if (interval.getSizeOnDisk() > targetNode.getNodeFreeSpace()) { |
| /* Nope, not enough room. Insert in a new sibling instead. */ |
| addSiblingNode(depth, getNewBranchStart(depth, interval)); |
| tryInsertAtNode(interval, getLatestBranch().size() - 1); |
| return; |
| } |
| |
| /* Make sure the interval time range fits this node */ |
| if (interval.getStart() < targetNode.getNodeStart()) { |
| /* |
| * No, this interval starts before the startTime of this node. We |
| * need to check recursively in parents if it can fit. |
| */ |
| tryInsertAtNode(interval, depth - 1); |
| return; |
| } |
| |
| /* |
| * Ok, there is room, and the interval fits in this time slot. Let's add |
| * it. |
| */ |
| targetNode.add(interval); |
| |
| updateEndTime(interval); |
| } |
| |
| /** |
| * Informs the tree that the insertion is requested at a given depth. When |
| * this is called, the element is not yet inserted, but the last call to |
| * this for an element will represent the depth at which is was really |
| * inserted. By default, this method does nothing and should not be |
| * necessary for concrete implementations, but it can be used by unit tests |
| * to check to position of insertion of elements. |
| * |
| * @param depth |
| * The depth at which the last insertion was done |
| */ |
| @VisibleForTesting |
| protected void informInsertingAtDepth(int depth) { |
| |
| } |
| |
| /** |
| * Get the start time of the new node of the branch that will be added |
| * starting at depth. |
| * |
| * Note that the depth is the depth of the last node that was filled and to |
| * which a sibling should be added. But depending on the returned start |
| * time, the actual new branch may start at a lower depth if the start time |
| * happens to be lesser than the parent's start time. |
| * |
| * @param depth |
| * The depth of the last node that was filled and at which the |
| * new branch should start. |
| * @param interval |
| * The interval that is about to be inserted |
| * @return The value that should be the start time of the sibling node |
| */ |
| protected abstract long getNewBranchStart(int depth, E interval); |
| |
| /** |
| * Method to add a sibling to any node in the latest branch. This will add |
| * children back down to the leaf level, if needed. |
| * |
| * @param depth |
| * The depth in latestBranch where we start adding |
| * @param newNodeStartTime |
| * The start time of the new node |
| */ |
| private final void addSiblingNode(int depth, long newNodeStartTime) { |
| synchronized (fLatestBranch) { |
| final long splitTime = fTreeEnd; |
| fLatestBranchSnapshot = null; |
| |
| if (depth >= fLatestBranch.size()) { |
| /* |
| * We need to make sure (indexOfNode - 1) doesn't get the last |
| * node in the branch, because that one is a Leaf Node. |
| */ |
| throw new IllegalStateException(); |
| } |
| |
| /* Check if we need to add a new root node */ |
| if (depth == 0) { |
| addNewRootNode(newNodeStartTime); |
| return; |
| } |
| |
| /* |
| * Check if we can indeed add a child to the target parent and if |
| * the new start time is not before the target parent. |
| */ |
| if (fLatestBranch.get(depth - 1).getNbChildren() == fMaxChildren || |
| newNodeStartTime < fLatestBranch.get(depth - 1).getNodeStart()) { |
| /* If not, add a branch starting one level higher instead */ |
| addSiblingNode(depth - 1, newNodeStartTime); |
| return; |
| } |
| |
| /* |
| * Close nodes from the leaf up because some parent nodes may need |
| * to get updated when their children are closed |
| */ |
| for (int i = fLatestBranch.size() - 1; i >= depth; i--) { |
| fLatestBranch.get(i).closeThisNode(splitTime); |
| fTreeIO.writeNode(fLatestBranch.get(i)); |
| } |
| |
| /* Split off the new branch from the old one */ |
| for (int i = depth; i < fLatestBranch.size(); i++) { |
| N prevNode = fLatestBranch.get(i - 1); |
| N newNode; |
| |
| switch (fLatestBranch.get(i).getNodeType()) { |
| case CORE: |
| newNode = initNewCoreNode(prevNode.getSequenceNumber(), newNodeStartTime); |
| break; |
| case LEAF: |
| newNode = initNewLeafNode(prevNode.getSequenceNumber(), newNodeStartTime); |
| break; |
| default: |
| throw new IllegalStateException(); |
| } |
| |
| prevNode.linkNewChild(newNode); |
| fLatestBranch.set(i, newNode); |
| } |
| } |
| } |
| |
| /** |
| * Similar to the previous method, except here we rebuild a completely new |
| * latestBranch |
| */ |
| private void addNewRootNode(long newNodeStartTime) { |
| final long nodeEnd = fTreeEnd; |
| |
| N oldRootNode = fLatestBranch.get(0); |
| N newRootNode = initNewCoreNode(-1, fTreeStart); |
| |
| /* Tell the old root node that it isn't root anymore */ |
| oldRootNode.setParentSequenceNumber(newRootNode.getSequenceNumber()); |
| |
| /* Close off the whole current latestBranch */ |
| for (int i = fLatestBranch.size() - 1; i >= 0; i--) { |
| fLatestBranch.get(i).closeThisNode(nodeEnd); |
| fTreeIO.writeNode(fLatestBranch.get(i)); |
| } |
| |
| /* Link the new root to its first child (the previous root node) */ |
| newRootNode.linkNewChild(oldRootNode); |
| |
| /* Rebuild a new latestBranch */ |
| int depth = fLatestBranch.size(); |
| fLatestBranch.clear(); |
| fLatestBranch.add(newRootNode); |
| |
| // Create new coreNode |
| for (int i = 1; i < depth; i++) { |
| N prevNode = fLatestBranch.get(i - 1); |
| N newNode = initNewCoreNode(prevNode.getSequenceNumber(), newNodeStartTime); |
| prevNode.linkNewChild(newNode); |
| fLatestBranch.add(newNode); |
| } |
| |
| // Create the new leafNode |
| N prevNode = fLatestBranch.get(depth - 1); |
| N newNode = initNewLeafNode(prevNode.getSequenceNumber(), newNodeStartTime); |
| prevNode.linkNewChild(newNode); |
| fLatestBranch.add(newNode); |
| } |
| |
| /** |
| * Update the tree's end time with this interval data |
| * |
| * @param interval |
| * The interval that was just added to the tree |
| */ |
| protected void updateEndTime(E interval) { |
| fTreeEnd = Math.max(fTreeEnd, interval.getEnd()); |
| } |
| |
| @Override |
| public void closeTree(long requestedEndTime) { |
| /* This is an important operation, queries can wait */ |
| synchronized (fLatestBranch) { |
| /* |
| * Work-around the "empty branches" that get created when the root |
| * node becomes full. Overwrite the tree's end time with the |
| * original wanted end-time, to ensure no queries are sent into |
| * those empty nodes. |
| */ |
| fTreeEnd = requestedEndTime; |
| |
| /* Close off the latest branch of the tree */ |
| for (int i = fLatestBranch.size() - 1; i >= 0; i--) { |
| fLatestBranch.get(i).closeThisNode(fTreeEnd); |
| fTreeIO.writeNode(fLatestBranch.get(i)); |
| } |
| |
| try (FileOutputStream fc = fTreeIO.getFileWriter(-1);) { |
| ByteBuffer buffer = ByteBuffer.allocate(TREE_HEADER_SIZE); |
| buffer.order(ByteOrder.LITTLE_ENDIAN); |
| buffer.clear(); |
| |
| buffer.putInt(getMagicNumber()); |
| |
| buffer.putInt(getFileVersion()); |
| buffer.putInt(fProviderVersion); |
| |
| buffer.putInt(fBlockSize); |
| buffer.putInt(fMaxChildren); |
| |
| buffer.putInt(fNodeCount); |
| |
| /* root node seq. nb */ |
| buffer.putInt(fLatestBranch.get(0).getSequenceNumber()); |
| |
| /* start time of this history */ |
| buffer.putLong(fLatestBranch.get(0).getNodeStart()); |
| |
| buffer.flip(); |
| fc.write(buffer.array()); |
| /* done writing the file header */ |
| |
| } catch (IOException e) { |
| /* |
| * If we were able to write so far, there should not be any |
| * problem at this point... |
| */ |
| throw new RuntimeException("State system write error"); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| @Override |
| public Iterable<E> getMatchingIntervals(TimeRangeCondition timeCondition, |
| Predicate<E> extraPredicate) { |
| |
| // TODO Change this to evaluate the nodes lazily |
| |
| List<E> intervalsOfNodes = new ArrayList<>(); |
| |
| /* Queue is a stack of nodes containing nodes intersecting t */ |
| Deque<Integer> queue = new LinkedList<>(); |
| /* We start by reading the information in the root node */ |
| queue.add(getRootNode().getSequenceNumber()); |
| |
| /* Then we follow the down in the relevant children */ |
| try { |
| while (!queue.isEmpty()) { |
| int sequenceNumber = queue.pop(); |
| HTNode<E> currentNode = readNode(sequenceNumber); |
| TimeRangeCondition nodeCondition = timeCondition.subCondition( |
| currentNode.getNodeStart(), currentNode.getNodeEnd()); |
| |
| if (nodeCondition == null) { |
| continue; |
| } |
| |
| if (currentNode.getNodeType() == HTNode.NodeType.CORE) { |
| /* Here we add the relevant children nodes for BFS */ |
| queue.addAll(currentNode.selectNextChildren(nodeCondition, currentNode.getCoreDataPredicate(extraPredicate))); |
| } |
| Collection<E> nodeIntervals = currentNode.getMatchingIntervals(nodeCondition, extraPredicate); |
| intervalsOfNodes.addAll(nodeIntervals); |
| } |
| } catch (ClosedChannelException e) { |
| } |
| return intervalsOfNodes; |
| } |
| |
| @Override |
| public @Nullable E getMatchingInterval(TimeRangeCondition timeCondition, |
| Predicate<E> extraPredicate) { |
| |
| /* Queue a stack of nodes containing nodes intersecting t */ |
| Deque<Integer> queue = new LinkedList<>(); |
| /* We start by reading the information in the root node */ |
| queue.add(getRootNode().getSequenceNumber()); |
| |
| /* Then we follow the down in the relevant children until we find the interval */ |
| try { |
| while (!queue.isEmpty()) { |
| int sequenceNumber = queue.pop(); |
| HTNode<E> currentNode = readNode(sequenceNumber); |
| |
| @Nullable E interval = currentNode.getMatchingInterval(timeCondition, extraPredicate); |
| if (interval != null) { |
| return interval; |
| } |
| |
| if (currentNode.getNodeType() == HTNode.NodeType.CORE) { |
| /* Here we add the relevant children nodes for BFS */ |
| queue.addAll(currentNode.selectNextChildren(timeCondition)); |
| } |
| } |
| } catch (ClosedChannelException e) { |
| } |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| return "Information on the current tree:\n\n" + "Blocksize: " //$NON-NLS-1$ //$NON-NLS-2$ |
| + fBlockSize + "\n" + "Max nb. of children per node: " //$NON-NLS-1$//$NON-NLS-2$ |
| + fMaxChildren + "\n" + "Number of nodes: " + fNodeCount //$NON-NLS-1$//$NON-NLS-2$ |
| + "\n" + "Depth of the tree: " + fLatestBranch.size() + "\n" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| + "Size of the treefile: " + getFileSize() + "\n" //$NON-NLS-1$//$NON-NLS-2$ |
| + "Root node has sequence number: " //$NON-NLS-1$ |
| + fLatestBranch.get(0).getSequenceNumber() + "\n" //$NON-NLS-1$ |
| + "'Latest leaf' has sequence number: " //$NON-NLS-1$ |
| + fLatestBranch.get(fLatestBranch.size() - 1).getSequenceNumber(); |
| } |
| |
| |
| // ------------------------------------------------------------------------ |
| // Test-specific methods |
| // ------------------------------------------------------------------------ |
| |
| /** |
| * Get the current depth of the tree. |
| * |
| * @return The current depth |
| */ |
| @VisibleForTesting |
| protected int getDepth() { |
| return getLatestBranch().size(); |
| } |
| |
| /** |
| * Get the leaf (bottom-most) node of the latest branch. |
| * |
| * @return The latest leaf |
| */ |
| @VisibleForTesting |
| protected N getLatestLeaf() { |
| List<N> latestBranch = getLatestBranch(); |
| return latestBranch.get(latestBranch.size() - 1); |
| } |
| |
| /** |
| * Verify a node's specific information about a child. |
| * |
| * @param parent |
| * The parent node |
| * @param index |
| * The index of the child in the parent's extra data |
| * @param child |
| * The child node to verify |
| * @return False if a problem was found, true otherwise |
| */ |
| @VisibleForTesting |
| protected boolean verifyChildrenSpecific(N parent, |
| int index, |
| N child) { |
| // Nothing to do for the default implementation |
| return true; |
| } |
| |
| /** |
| * This method should verify in the whole time range of the parent node that |
| * the child node appears or not as a next children for a given timestamp. |
| * |
| * @param parent |
| * The parent node |
| * @param child |
| * The child node |
| * @return False if a problem was found, true otherwise |
| */ |
| @VisibleForTesting |
| protected boolean verifyIntersectingChildren(N parent, N child) { |
| int childSequence = child.getSequenceNumber(); |
| boolean shouldBeInCollection; |
| Collection<Integer> nextChildren; |
| for (long t = parent.getNodeStart(); t < parent.getNodeEnd(); t++) { |
| shouldBeInCollection = true; |
| nextChildren = parent.selectNextChildren(TimeRangeCondition.singleton(t)); |
| if (shouldBeInCollection != nextChildren.contains(childSequence)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |