blob: 5e2ec09bb4835b43ecbf64ec2d29996c2d6c5445 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2016 Ericsson
* Copyright (c) 2010, 2011 École Polytechnique de Montréal
* Copyright (c) 2010, 2011 Alexandre Montplaisir <alexandre.montplaisir@gmail.com>
*
* 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:
* Alexandre Montplaisir - Initial API and implementation
* Patrick Tasse - Add message to exceptions
*******************************************************************************/
package org.eclipse.tracecompass.internal.statesystem.core.backend.historytree;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.common.core.log.TraceCompassLog;
import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils;
import org.eclipse.tracecompass.internal.provisional.datastore.core.condition.IntegerRangeCondition;
import org.eclipse.tracecompass.internal.provisional.datastore.core.condition.TimeRangeCondition;
import org.eclipse.tracecompass.internal.statesystem.core.Activator;
import org.eclipse.tracecompass.statesystem.core.backend.IStateHistoryBackend;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import com.google.common.annotations.VisibleForTesting;
/**
* History Tree backend for storing a state history. This is the basic version
* that runs in the same thread as the class creating it.
*
* @author Alexandre Montplaisir
*/
public class HistoryTreeBackend implements IStateHistoryBackend {
private static final @NonNull Logger LOGGER = TraceCompassLog.getLogger(HistoryTreeBackend.class);
private final @NonNull String fSsid;
/**
* The history tree that sits underneath.
*/
private final @NonNull IHistoryTree fSht;
/** Indicates if the history tree construction is done */
private volatile boolean fFinishedBuilding = false;
/**
* Indicates if the history tree construction is done
*
* @return if the history tree construction is done
*/
protected boolean isFinishedBuilding() {
return fFinishedBuilding;
}
/**
* Sets if the history tree is finished building
*
* @param isFinishedBuilding
* is the history tree finished building
*/
protected void setFinishedBuilding(boolean isFinishedBuilding) {
fFinishedBuilding = isFinishedBuilding;
}
/**
* Constructor for new history files. Use this when creating a new history
* from scratch.
*
* @param ssid
* The state system's ID
* @param newStateFile
* The filename/location where to store the state history (Should
* end in .ht)
* @param providerVersion
* Version of of the state provider. We will only try to reopen
* existing files if this version matches the one in the
* framework.
* @param startTime
* The earliest time stamp that will be stored in the history
* @param blockSize
* The size of the blocks in the history file. This should be a
* multiple of 4096.
* @param maxChildren
* The maximum number of children each core node can have
* @throws IOException
* Thrown if we can't create the file for some reason
*/
public HistoryTreeBackend(@NonNull String ssid,
File newStateFile,
int providerVersion,
long startTime,
int blockSize,
int maxChildren) throws IOException {
fSsid = ssid;
final HTConfig conf = new HTConfig(newStateFile, blockSize, maxChildren,
providerVersion, startTime);
fSht = initializeSHT(conf);
}
/**
* Constructor for new history files. Use this when creating a new history
* from scratch. This version supplies sane defaults for the configuration
* parameters.
*
* @param ssid
* The state system's id
* @param newStateFile
* The filename/location where to store the state history (Should
* end in .ht)
* @param providerVersion
* Version of of the state provider. We will only try to reopen
* existing files if this version matches the one in the
* framework.
* @param startTime
* The earliest time stamp that will be stored in the history
* @throws IOException
* Thrown if we can't create the file for some reason
* @since 1.0
*/
public HistoryTreeBackend(@NonNull String ssid, File newStateFile, int providerVersion, long startTime)
throws IOException {
this(ssid, newStateFile, providerVersion, startTime, 64 * 1024, 50);
}
/**
* Existing history constructor. Use this to open an existing state-file.
*
* @param ssid
* The state system's id
* @param existingStateFile
* Filename/location of the history we want to load
* @param providerVersion
* Expected version of of the state provider plugin.
* @throws IOException
* If we can't read the file, if it doesn't exist, is not
* recognized, or if the version of the file does not match the
* expected providerVersion.
*/
public HistoryTreeBackend(@NonNull String ssid, @NonNull File existingStateFile, int providerVersion)
throws IOException {
fSsid = ssid;
fSht = initializeSHT(existingStateFile, providerVersion);
fFinishedBuilding = true;
}
/**
* New-tree initializer for the History Tree wrapped by this backend. Can be
* overriden to use different implementations.
*
* @param conf
* The HTConfig configuration object
* @return The new history tree
* @throws IOException
* If there was a problem during creation
*/
@VisibleForTesting
protected @NonNull IHistoryTree initializeSHT(@NonNull HTConfig conf) throws IOException {
TraceCompassLogUtils.traceObjectCreation(LOGGER, Level.FINER, this);
return HistoryTreeFactory.createHistoryTree(conf);
}
/**
* Existing-tree initializer for the History Tree wrapped by this backend.
* Can be overriden to use different implementations.
*
* @param existingStateFile
* The file to open
* @param providerVersion
* The expected state provider version
* @return The history tree opened from the given file
* @throws IOException
* If there was a problem during creation
*/
@VisibleForTesting
protected @NonNull IHistoryTree initializeSHT(@NonNull File existingStateFile, int providerVersion) throws IOException {
return HistoryTreeFactory.createFromFile(existingStateFile.toPath(), providerVersion);
}
/**
* Get the History Tree built by this backend.
*
* Note: Do not override this method. If you want to extend the class to use
* a different History Tree implementation, override both variants of
* {@link #initializeSHT} instead.
*
* @return The history tree
*/
protected final @NonNull IHistoryTree getSHT() {
return fSht;
}
@Override
public String getSSID() {
return fSsid;
}
@Override
public long getStartTime() {
return getSHT().getTreeStart();
}
@Override
public long getEndTime() {
return getSHT().getTreeEnd();
}
@Override
public void insertPastState(long stateStartTime, long stateEndTime,
int quark, Object value) throws TimeRangeException {
HTInterval interval = new HTInterval(stateStartTime, stateEndTime,
quark, value);
/* Start insertions at the "latest leaf" */
getSHT().insertInterval(interval);
}
@Override
public void finishedBuilding(long endTime) {
getSHT().closeTree(endTime);
fFinishedBuilding = true;
}
@Override
public FileInputStream supplyAttributeTreeReader() {
return getSHT().supplyATReader();
}
@Override
public File supplyAttributeTreeWriterFile() {
return getSHT().supplyATWriterFile();
}
@Override
public long supplyAttributeTreeWriterFilePosition() {
return getSHT().supplyATWriterFilePos();
}
@Override
public void removeFiles() {
getSHT().deleteFile();
}
@Override
public void dispose() {
if (fFinishedBuilding) {
TraceCompassLogUtils.traceInstant(LOGGER, Level.FINE, "HistoryTreeBackend:ClosingFile", "size", getSHT().getFileSize()); //$NON-NLS-1$ //$NON-NLS-2$
TraceCompassLogUtils.traceObjectDestruction(LOGGER, Level.FINER, this);
getSHT().closeFile();
} else {
/*
* The build is being interrupted, delete the file we partially
* built since it won't be complete, so shouldn't be re-used in the
* future (.deleteFile() will close the file first)
*/
getSHT().deleteFile();
}
}
@Override
public void doQuery(List<ITmfStateInterval> stateInfo, long t)
throws TimeRangeException, StateSystemDisposedException {
checkValidTime(t);
/* Queue is a stack of nodes containing nodes intersecting t */
Deque<Integer> queue = new ArrayDeque<>();
/* We start by reading the information in the root node */
queue.add(getSHT().getRootNode().getSequenceNumber());
/* Then we follow the down in the relevant children */
try {
while (!queue.isEmpty()) {
int sequenceNumber = queue.pop();
HTNode currentNode = getSHT().readNode(sequenceNumber);
if (currentNode.getNodeType() == HTNode.NodeType.CORE) {
/* Here we add the relevant children nodes for BFS */
queue.addAll(((ParentNode) currentNode).selectNextChildren(t));
}
currentNode.writeInfoFromNode(stateInfo, t);
}
} catch (ClosedChannelException e) {
throw new StateSystemDisposedException(e);
}
/*
* The stateInfo should now be filled with everything needed, we pass
* the control back to the State System.
*/
}
@Override
public ITmfStateInterval doSingularQuery(long t, int attributeQuark)
throws TimeRangeException, StateSystemDisposedException {
try {
return getRelevantInterval(t, attributeQuark);
} catch (ClosedChannelException e) {
throw new StateSystemDisposedException(e);
}
}
private void checkValidTime(long t) {
long startTime = getStartTime();
long endTime = getEndTime();
if (t < startTime || t > endTime) {
throw new TimeRangeException(String.format("%s Time:%d, Start:%d, End:%d", //$NON-NLS-1$
fSsid, t, startTime, endTime));
}
}
/**
* Inner method to find the interval in the tree containing the requested
* key/timestamp pair, wherever in which node it is.
*/
private HTInterval getRelevantInterval(long t, int key)
throws TimeRangeException, ClosedChannelException {
checkValidTime(t);
Deque<Integer> queue = new ArrayDeque<>();
queue.add(getSHT().getRootNode().getSequenceNumber());
HTInterval interval = null;
while (interval == null && !queue.isEmpty()) {
int sequenceNumber = queue.pop();
HTNode currentNode = getSHT().readNode(sequenceNumber);
if (currentNode.getNodeType() == HTNode.NodeType.CORE) {
/* Here we add the relevant children nodes for BFS */
queue.addAll(((ParentNode) currentNode).selectNextChildren(t, key));
}
interval = currentNode.getRelevantInterval(key, t);
}
return interval;
}
@Override
public Iterable<@NonNull ITmfStateInterval> query2D(IntegerRangeCondition quarks, TimeRangeCondition times) {
return query2D(quarks, times, false);
}
@Override
public Iterable<@NonNull ITmfStateInterval> query2D(IntegerRangeCondition quarks, TimeRangeCondition times, boolean reverse) {
try (TraceCompassLogUtils.FlowScopeLog log = new TraceCompassLogUtils.FlowScopeLogBuilder(LOGGER, Level.FINER,
"HistoryTreeBackend:query2D:init", //$NON-NLS-1$
"ssid", getSSID(), //$NON-NLS-1$
"quarks", quarks, //$NON-NLS-1$
"timeCondition", times).build()) { //$NON-NLS-1$
return () -> new Iterator<@NonNull ITmfStateInterval>() {
private final Deque<Integer> seqNumberQueue = new ArrayDeque<>(Collections.singleton(getSHT().getRootNode().getSequenceNumber()));
private Iterator<@NonNull HTInterval> intervalQueue = Collections.emptyIterator();
@Override
public boolean hasNext() {
while (!intervalQueue.hasNext() && !seqNumberQueue.isEmpty()) {
try {
HTNode currentNode = getSHT().readNode(seqNumberQueue);
/*
* Compute reduced conditions here to reduce complexity in queuing operations.
*/
TimeRangeCondition subTimes = times.subCondition(currentNode.getNodeStart(), currentNode.getNodeEnd());
/*
* During the SHT construction, the bounds of the children are not final, so we
* may have queued some nodes which don't overlap the query.
*/
if (quarks.intersects(currentNode.getMinQuark(), currentNode.getMaxQuark()) && subTimes != null) {
if (currentNode.getNodeType() == HTNode.NodeType.CORE) {
// Queue the relevant children nodes for BFS.
((ParentNode) currentNode).queueNextChildren2D(quarks, subTimes, seqNumberQueue, reverse);
}
intervalQueue = currentNode.iterable2D(quarks, subTimes).iterator();
}
} catch (ClosedChannelException e) {
try (TraceCompassLogUtils.FlowScopeLog closedChannelLog = new TraceCompassLogUtils.FlowScopeLogBuilder(LOGGER, Level.FINER,
"HistoryTreeBackend:query2D:channelClosed").setParentScope(log).build()) { //$NON-NLS-1$
return false;
}
}
}
boolean hasNext = intervalQueue.hasNext();
if (!hasNext) {
try (TraceCompassLogUtils.FlowScopeLog noNext = new TraceCompassLogUtils.FlowScopeLogBuilder(LOGGER, Level.FINER,
"HistoryTreeBackend:query2D:iteratorEnd").setParentScope(log).build()) { //$NON-NLS-1$
}
}
return intervalQueue.hasNext();
}
@Override
public ITmfStateInterval next() {
return intervalQueue.next();
}
};
}
}
/**
* Return the size of the tree history file
*
* @return The current size of the history file in bytes
*/
public long getFileSize() {
return getSHT().getFileSize();
}
/**
* Return the average node usage as a percentage (between 0 and 100)
*
* @return Average node usage %
*/
public int getAverageNodeUsage() {
HTNode node;
long total = 0;
long ret;
try {
for (int seq = 0; seq < getSHT().getNodeCount(); seq++) {
node = getSHT().readNode(seq);
total += node.getNodeUsagePercent();
}
} catch (ClosedChannelException e) {
Activator.getDefault().logError(e.getMessage(), e);
}
ret = total / getSHT().getNodeCount();
/* The return value should be a percentage */
if (ret < 0 || ret > 100) {
throw new IllegalStateException("Average node usage is not a percentage: " + ret); //$NON-NLS-1$
}
return (int) ret;
}
}