/*******************************************************************************
* Copyright (c) 2010, 2014 Ericsson, École Polytechnique de Montréal, and others
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexandre Montplaisir - Initial API and implementation
* Florian Wininger - Add Extension and Leaf Node
*******************************************************************************/
package fr.inria.linuxtools.internal.statesystem.core.backend.historytree;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
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.Collections;
import java.util.List;
import fr.inria.linuxtools.internal.statesystem.core.Activator;
import fr.inria.linuxtools.statesystem.core.ITmfStateSystemBuilder;
import fr.inria.linuxtools.statesystem.core.exceptions.TimeRangeException;
/**
* Meta-container for the History Tree. This structure contains all the
* high-level data relevant to the tree.
*
* @author Alexandre Montplaisir
*/
public class HistoryTree {
/**
* Size of the "tree header" in the tree-file The nodes will use this offset
* to know where they should be in the file. This should always be a
* multiple of 4K.
*/
public static final int TREE_HEADER_SIZE = 4096;
private static final int HISTORY_FILE_MAGIC_NUMBER = 0x05FFA900;
/** File format version. Increment when breaking compatibility. */
private static final int FILE_VERSION = 4;
// ------------------------------------------------------------------------
// Tree-specific configuration
// ------------------------------------------------------------------------
/** Container for all the configuration constants */
private final HTConfig config;
/** Reader/writer object */
private final HT_IO treeIO;
// ------------------------------------------------------------------------
// Variable Fields (will change throughout the existence of the SHT)
// ------------------------------------------------------------------------
/** Latest timestamp found in the tree (at any given moment) */
private long treeEnd;
/** The total number of nodes that exists in this tree */
private int nodeCount;
/** "Cache" to keep the active nodes in memory */
private final List<HTNode> latestBranch;
// ------------------------------------------------------------------------
// Constructors/"Destructors"
// ------------------------------------------------------------------------
/**
* Create a new State History from scratch, using a {@link HTConfig} object
* for configuration.
*
* @param conf
* The config to use for this History Tree.
* @throws IOException
* If an error happens trying to open/write to the file
* specified in the config
*/
public HistoryTree(HTConfig conf) throws IOException {
/*
* Simple check to make sure we have enough place in the 0th block for
* the tree configuration
*/
if (conf.getBlockSize() < TREE_HEADER_SIZE) {
throw new IllegalArgumentException();
}
config = conf;
treeEnd = conf.getTreeStart();
nodeCount = 0;
latestBranch = Collections.synchronizedList(new ArrayList<HTNode>());
/* Prepare the IO object */
treeIO = new HT_IO(config, true);
/* Add the first node to the tree */
LeafNode firstNode = initNewLeafNode(-1, conf.getTreeStart());
latestBranch.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 expProviderVersion
* The expected version of the state provider
* @throws IOException
* If an error happens reading the file
*/
public HistoryTree(File existingStateFile, int expProviderVersion) 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();
fc.read(buffer);
buffer.flip();
/*
* Check the magic number to make sure we're opening the right type
* of file
*/
res = buffer.getInt();
if (res != HISTORY_FILE_MAGIC_NUMBER) {
throw new IOException("Wrong magic number"); //$NON-NLS-1$
}
res = buffer.getInt(); /* File format version number */
if (res != FILE_VERSION) {
throw new IOException("Mismatching History Tree file format versions"); //$NON-NLS-1$
}
res = buffer.getInt(); /* Event handler's version number */
if (res != expProviderVersion &&
expProviderVersion != ITmfStateSystemBuilder.IGNORE_PROVIDER_VERSION) {
/*
* 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 */
this.nodeCount = buffer.getInt();
rootNodeSeqNb = buffer.getInt();
startTime = buffer.getLong();
this.config = new HTConfig(existingStateFile, bs, maxc, expProviderVersion, 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?
*/
this.treeIO = new HT_IO(config, false);
this.latestBranch = buildLatestBranch(rootNodeSeqNb);
this.treeEnd = 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<HTNode> buildLatestBranch(int rootNodeSeqNb) throws ClosedChannelException {
List<HTNode> list = new ArrayList<>();
HTNode nextChildNode = treeIO.readNode(rootNodeSeqNb);
list.add(nextChildNode);
/* Follow the last branch up to the leaf */
while (nextChildNode.getNodeType() == HTNode.NodeType.CORE) {
nextChildNode = treeIO.readNode(((CoreNode) nextChildNode).getLatestChild());
list.add(nextChildNode);
}
return Collections.synchronizedList(list);
}
/**
* "Save" the tree to disk. This method will cause the treeIO object to
* commit all nodes to disk and then return the RandomAccessFile descriptor
* so the Tree object can save its configuration into the header of the
* file.
*
* @param requestedEndTime
* The greatest timestamp present in the history tree
*/
public void closeTree(long requestedEndTime) {
/* This is an important operation, queries can wait */
synchronized (latestBranch) {
/*
* 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.
*
* This won't be needed once extended nodes are implemented.
*/
this.treeEnd = requestedEndTime;
/* Close off the latest branch of the tree */
for (int i = 0; i < latestBranch.size(); i++) {
latestBranch.get(i).closeThisNode(treeEnd);
treeIO.writeNode(latestBranch.get(i));
}
try (FileChannel fc = treeIO.getFcOut();) {
ByteBuffer buffer = ByteBuffer.allocate(TREE_HEADER_SIZE);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.clear();
/* Save the config of the tree to the header of the file */
fc.position(0);
buffer.putInt(HISTORY_FILE_MAGIC_NUMBER);
buffer.putInt(FILE_VERSION);
buffer.putInt(config.getProviderVersion());
buffer.putInt(config.getBlockSize());
buffer.putInt(config.getMaxChildren());
buffer.putInt(nodeCount);
/* root node seq. nb */
buffer.putInt(latestBranch.get(0).getSequenceNumber());
/* start time of this history */
buffer.putLong(latestBranch.get(0).getNodeStart());
buffer.flip();
int res = fc.write(buffer);
assert (res <= TREE_HEADER_SIZE);
/* 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$
}
}
}
// ------------------------------------------------------------------------
// Accessors
// ------------------------------------------------------------------------
/**
* Get the start time of this tree.
*
* @return The start time
*/
public long getTreeStart() {
return config.getTreeStart();
}
/**
* Get the current end time of this tree.
*
* @return The end time
*/
public long getTreeEnd() {
return treeEnd;
}
/**
* Get the number of nodes in this tree.
*
* @return The number of nodes
*/
public int getNodeCount() {
return nodeCount;
}
/**
* Get the current root node of this tree
*
* @return The root node
*/
public HTNode getRootNode() {
return latestBranch.get(0);
}
// ------------------------------------------------------------------------
// HT_IO interface
// ------------------------------------------------------------------------
/**
* Return the FileInputStream reader with which we will read an attribute
* tree (it will be sought to the correct position).
*
* @return The FileInputStream indicating the file and position from which
* the attribute tree can be read.
*/
public FileInputStream supplyATReader() {
return treeIO.supplyATReader(getNodeCount());
}
/**
* Return the file to which we will write the attribute tree.
*
* @return The file to which we will write the attribute tree
*/
public File supplyATWriterFile() {
return config.getStateFile();
}
/**
* Return the position in the file (given by {@link #supplyATWriterFile})
* where to start writing the attribute tree.
*
* @return The position in the file where to start writing
*/
public long supplyATWriterFilePos() {
return HistoryTree.TREE_HEADER_SIZE
+ ((long) getNodeCount() * config.getBlockSize());
}
/**
* 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 HTNode readNode(int seqNumber) throws ClosedChannelException {
/* Try to read the node from memory */
synchronized (latestBranch) {
for (HTNode node : latestBranch) {
if (node.getSequenceNumber() == seqNumber) {
return node;
}
}
}
/* Read the node from disk */
return treeIO.readNode(seqNumber);
}
/**
* Write a node object to the history file.
*
* @param node
* The node to write to disk
*/
public void writeNode(HTNode node) {
treeIO.writeNode(node);
}
/**
* Close the history file.
*/
public void closeFile() {
treeIO.closeFile();
}
/**
* Delete the history file.
*/
public void deleteFile() {
treeIO.deleteFile();
}
// ------------------------------------------------------------------------
// Operations
// ------------------------------------------------------------------------
/**
* Insert an interval in the tree.
*
* @param interval
* The interval to be inserted
* @throws TimeRangeException
* If the start of end time of the interval are invalid
*/
public void insertInterval(HTInterval interval) throws TimeRangeException {
if (interval.getStartTime() < config.getTreeStart()) {
throw new TimeRangeException();
}
tryInsertAtNode(interval, latestBranch.size() - 1);
}
/**
* Inner method to find in which node we should add the interval.
*
* @param interval
* The interval to add to the tree
* @param indexOfNode
* The index *in the latestBranch* where we are trying the
* insertion
*/
private void tryInsertAtNode(HTInterval interval, int indexOfNode) {
HTNode targetNode = latestBranch.get(indexOfNode);
/* Verify if there is enough room in this node to store this interval */
if (interval.getIntervalSize() > targetNode.getNodeFreeSpace()) {
/* Nope, not enough room. Insert in a new sibling instead. */
addSiblingNode(indexOfNode);
tryInsertAtNode(interval, latestBranch.size() - 1);
return;
}
/* Make sure the interval time range fits this node */
if (interval.getStartTime() < targetNode.getNodeStart()) {
/*
* No, this interval starts before the startTime of this node. We
* need to check recursively in parents if it can fit.
*/
assert (indexOfNode >= 1);
tryInsertAtNode(interval, indexOfNode - 1);
return;
}
/*
* Ok, there is room, and the interval fits in this time slot. Let's add
* it.
*/
targetNode.addInterval(interval);
/* Update treeEnd if needed */
if (interval.getEndTime() > this.treeEnd) {
this.treeEnd = interval.getEndTime();
}
}
/**
* 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 indexOfNode
* The index in latestBranch where we start adding
*/
private void addSiblingNode(int indexOfNode) {
synchronized (latestBranch) {
final long splitTime = treeEnd;
if (indexOfNode >= latestBranch.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 (indexOfNode == 0) {
addNewRootNode();
return;
}
/* Check if we can indeed add a child to the target parent */
if (((CoreNode) latestBranch.get(indexOfNode - 1)).getNbChildren() == config.getMaxChildren()) {
/* If not, add a branch starting one level higher instead */
addSiblingNode(indexOfNode - 1);
return;
}
/* Split off the new branch from the old one */
for (int i = indexOfNode; i < latestBranch.size(); i++) {
latestBranch.get(i).closeThisNode(splitTime);
treeIO.writeNode(latestBranch.get(i));
CoreNode prevNode = (CoreNode) latestBranch.get(i - 1);
HTNode newNode;
switch (latestBranch.get(i).getNodeType()) {
case CORE:
newNode = initNewCoreNode(prevNode.getSequenceNumber(), splitTime + 1);
break;
case LEAF:
newNode = initNewLeafNode(prevNode.getSequenceNumber(), splitTime + 1);
break;
default:
throw new IllegalStateException();
}
prevNode.linkNewChild(newNode);
latestBranch.set(i, newNode);
}
}
}
/**
* Similar to the previous method, except here we rebuild a completely new
* latestBranch
*/
private void addNewRootNode() {
final long splitTime = this.treeEnd;
HTNode oldRootNode = latestBranch.get(0);
CoreNode newRootNode = initNewCoreNode(-1, config.getTreeStart());
/* Tell the old root node that it isn't root anymore */
oldRootNode.setParentSequenceNumber(newRootNode.getSequenceNumber());
/* Close off the whole current latestBranch */
for (int i = 0; i < latestBranch.size(); i++) {
latestBranch.get(i).closeThisNode(splitTime);
treeIO.writeNode(latestBranch.get(i));
}
/* Link the new root to its first child (the previous root node) */
newRootNode.linkNewChild(oldRootNode);
/* Rebuild a new latestBranch */
int depth = latestBranch.size();
latestBranch.clear();
latestBranch.add(newRootNode);
// Create new coreNode
for (int i = 1; i < depth + 1; i++) {
CoreNode prevNode = (CoreNode) latestBranch.get(i - 1);
CoreNode newNode = initNewCoreNode(prevNode.getParentSequenceNumber(),
splitTime + 1);
prevNode.linkNewChild(newNode);
latestBranch.add(newNode);
}
// Create the new leafNode
CoreNode prevNode = (CoreNode) latestBranch.get(depth);
LeafNode newNode = initNewLeafNode(prevNode.getParentSequenceNumber(), splitTime + 1);
prevNode.linkNewChild(newNode);
latestBranch.add(newNode);
}
/**
* 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
*/
private CoreNode initNewCoreNode(int parentSeqNumber, long startTime) {
CoreNode newNode = new CoreNode(config, this.nodeCount, parentSeqNumber,
startTime);
this.nodeCount++;
/* Update the treeEnd if needed */
if (startTime >= this.treeEnd) {
this.treeEnd = startTime + 1;
}
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
*/
private LeafNode initNewLeafNode(int parentSeqNumber, long startTime) {
LeafNode newNode = new LeafNode(config, this.nodeCount, parentSeqNumber,
startTime);
this.nodeCount++;
/* Update the treeEnd if needed */
if (startTime >= this.treeEnd) {
this.treeEnd = startTime + 1;
}
return newNode;
}
/**
* Inner method to select the next child of the current node intersecting
* the given timestamp. Useful for moving down the tree following one
* branch.
*
* @param currentNode
* The node on which the request is made
* @param t
* The timestamp to choose which child is the next one
* @return The child node intersecting t
* @throws ClosedChannelException
* If the file channel was closed while we were reading the tree
*/
public HTNode selectNextChild(CoreNode currentNode, long t) throws ClosedChannelException {
assert (currentNode.getNbChildren() > 0);
int potentialNextSeqNb = currentNode.getSequenceNumber();
for (int i = 0; i < currentNode.getNbChildren(); i++) {
if (t >= currentNode.getChildStart(i)) {
potentialNextSeqNb = currentNode.getChild(i);
} else {
break;
}
}
/*
* Once we exit this loop, we should have found a children to follow. If
* we didn't, there's a problem.
*/
assert (potentialNextSeqNb != currentNode.getSequenceNumber());
/*
* Since this code path is quite performance-critical, avoid iterating
* through the whole latestBranch array if we know for sure the next
* node has to be on disk
*/
if (currentNode.isOnDisk()) {
return treeIO.readNode(potentialNextSeqNb);
}
return readNode(potentialNextSeqNb);
}
/**
* Get the current size of the history file.
*
* @return The history file size
*/
public long getFileSize() {
return config.getStateFile().length();
}
// ------------------------------------------------------------------------
// Test/debugging methods
// ------------------------------------------------------------------------
/**
* Debugging method to make sure all intervals contained in the given node
* have valid start and end times.
*
* @param zenode
* The node to check
* @return True if everything is fine, false if there is at least one
* invalid timestamp (end time < start time, time outside of the
* range of the node, etc.)
*/
@SuppressWarnings("nls")
public boolean checkNodeIntegrity(HTNode zenode) {
/* Only used for debugging, shouldn't be externalized */
HTNode otherNode;
CoreNode node;
StringBuffer buf = new StringBuffer();
boolean ret = true;
// FIXME /* Only testing Core Nodes for now */
if (!(zenode instanceof CoreNode)) {
return true;
}
node = (CoreNode) zenode;
try {
/*
* Test that this node's start and end times match the start of the
* first child and the end of the last child, respectively
*/
if (node.getNbChildren() > 0) {
otherNode = treeIO.readNode(node.getChild(0));
if (node.getNodeStart() != otherNode.getNodeStart()) {
buf.append("Start time of node (" + node.getNodeStart() + ") "
+ "does not match start time of first child " + "("
+ otherNode.getNodeStart() + "), " + "node #"
+ otherNode.getSequenceNumber() + ")\n");
ret = false;
}
if (node.isOnDisk()) {
otherNode = treeIO.readNode(node.getLatestChild());
if (node.getNodeEnd() != otherNode.getNodeEnd()) {
buf.append("End time of node (" + node.getNodeEnd()
+ ") does not match end time of last child ("
+ otherNode.getNodeEnd() + ", node #"
+ otherNode.getSequenceNumber() + ")\n");
ret = false;
}
}
}
/*
* Test that the childStartTimes[] array matches the real nodes'
* start times
*/
for (int i = 0; i < node.getNbChildren(); i++) {
otherNode = treeIO.readNode(node.getChild(i));
if (otherNode.getNodeStart() != node.getChildStart(i)) {
buf.append(" Expected start time of child node #"
+ node.getChild(i) + ": " + node.getChildStart(i)
+ "\n" + " Actual start time of node #"
+ otherNode.getSequenceNumber() + ": "
+ otherNode.getNodeStart() + "\n");
ret = false;
}
}
} catch (ClosedChannelException e) {
e.printStackTrace();
}
if (!ret) {
System.out.println("");
System.out.println("SHT: Integrity check failed for node #"
+ node.getSequenceNumber() + ":");
System.out.println(buf.toString());
}
return ret;
}
/**
* Check the integrity of all the nodes in the tree. Calls
* {@link #checkNodeIntegrity} for every node in the tree.
*/
public void checkIntegrity() {
try {
for (int i = 0; i < nodeCount; i++) {
checkNodeIntegrity(treeIO.readNode(i));
}
} catch (ClosedChannelException e) {
}
}
/* Only used for debugging, shouldn't be externalized */
@SuppressWarnings("nls")
@Override
public String toString() {
return "Information on the current tree:\n\n" + "Blocksize: "
+ config.getBlockSize() + "\n" + "Max nb. of children per node: "
+ config.getMaxChildren() + "\n" + "Number of nodes: " + nodeCount
+ "\n" + "Depth of the tree: " + latestBranch.size() + "\n"
+ "Size of the treefile: " + this.getFileSize() + "\n"
+ "Root node has sequence number: "
+ latestBranch.get(0).getSequenceNumber() + "\n"
+ "'Latest leaf' has sequence number: "
+ latestBranch.get(latestBranch.size() - 1).getSequenceNumber();
}
/**
* Start at currentNode and print the contents of all its children, in
* pre-order. Give the root node in parameter to visit the whole tree, and
* have a nice overview.
*/
/* Only used for debugging, shouldn't be externalized */
@SuppressWarnings("nls")
private void preOrderPrint(PrintWriter writer, boolean printIntervals,
HTNode currentNode, int curDepth) {
writer.println(currentNode.toString());
if (printIntervals) {
currentNode.debugPrintIntervals(writer);
}
switch (currentNode.getNodeType()) {
case LEAF:
/* Stop if it's the leaf node */
return;
case CORE:
try {
final CoreNode node = (CoreNode) currentNode;
/* Print the extensions, if any */
int extension = node.getExtensionSequenceNumber();
while (extension != -1) {
HTNode nextNode = treeIO.readNode(extension);
preOrderPrint(writer, printIntervals, nextNode, curDepth);
}
/* Print the child nodes */
for (int i = 0; i < node.getNbChildren(); i++) {
HTNode nextNode = treeIO.readNode(node.getChild(i));
for (int j = 0; j < curDepth; j++) {
writer.print(" ");
}
writer.print("+-");
preOrderPrint(writer, printIntervals, nextNode, curDepth + 1);
}
} catch (ClosedChannelException e) {
Activator.getDefault().logError(e.getMessage());
}
break;
default:
break;
}
}
/**
* Print out the full tree for debugging purposes
*
* @param writer
* PrintWriter in which to write the output
* @param printIntervals
* Flag to enable full output of the interval information
*/
public void debugPrintFullTree(PrintWriter writer, boolean printIntervals) {
/* Only used for debugging, shouldn't be externalized */
this.preOrderPrint(writer, false, latestBranch.get(0), 0);
if (printIntervals) {
writer.println("\nDetails of intervals:"); //$NON-NLS-1$
this.preOrderPrint(writer, true, latestBranch.get(0), 0);
}
writer.println('\n');
}
}