/******************************************************************************* * Copyright (c) 2010, 2017 É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 *******************************************************************************/ package org.eclipse.tracecompass.internal.datastore.core.historytree; import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tracecompass.common.core.log.TraceCompassLog; import org.eclipse.tracecompass.internal.datastore.core.Activator; import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.AbstractHistoryTree.IHTNodeFactory; import org.eclipse.tracecompass.internal.provisional.datastore.core.interval.IHTInterval; import org.eclipse.tracecompass.internal.provisional.datastore.core.interval.IHTIntervalReader; import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.HTNode; import org.eclipse.tracecompass.internal.provisional.datastore.core.historytree.IHistoryTree; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; /** * This class abstracts inputs/outputs of the HistoryTree nodes. * * It contains all the methods and descriptors to handle reading/writing nodes * to the tree-file on disk and all the caching mechanisms. * * This abstraction is mainly for code isolation/clarification purposes. Every * HistoryTree must contain 1 and only 1 HT_IO element. * * @author Alexandre Montplaisir * @author Geneviève Bastien * @param <E> * The type of objects that will be saved in the tree * @param <N> * The base type of the nodes of this tree */ public class HtIo<E extends IHTInterval, N extends HTNode<E>> { private static final Logger LOGGER = TraceCompassLog.getLogger(HtIo.class); // ------------------------------------------------------------------------ // Global cache of nodes // ------------------------------------------------------------------------ private static final class CacheKey { public final HtIo<IHTInterval, HTNode<IHTInterval>> fHistoryTreeIo; public final int fSeqNumber; public CacheKey(HtIo<IHTInterval, HTNode<IHTInterval>> htio, int seqNumber) { fHistoryTreeIo = htio; fSeqNumber = seqNumber; } @Override public int hashCode() { return Objects.hash(fHistoryTreeIo, fSeqNumber); } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } CacheKey other = (CacheKey) obj; return (fHistoryTreeIo.equals(other.fHistoryTreeIo) && fSeqNumber == other.fSeqNumber); } } private static final int CACHE_SIZE = 256; private static final LoadingCache<CacheKey, HTNode<IHTInterval>> NODE_CACHE = checkNotNull(CacheBuilder.newBuilder() .maximumSize(CACHE_SIZE) .build(new CacheLoader<CacheKey, HTNode<IHTInterval>>() { @Override public HTNode<IHTInterval> load(CacheKey key) throws IOException { HtIo<IHTInterval, HTNode<IHTInterval>> io = key.fHistoryTreeIo; int seqNb = key.fSeqNumber; LOGGER.finest(() -> "[HtIo:CacheMiss] seqNum=" + seqNb); //$NON-NLS-1$ synchronized (io) { io.seekFCToNodePos(io.fFileChannelIn, seqNb); return HTNode.readNode(io.fBlockSize, io.fNodeMaxChildren, io.fFileChannelIn, io.fObjectReader, io.fNodeFactory); } } })); /** * This method invalidates all data in the cache so nodes will have to be * read again */ @VisibleForTesting static void clearCache() { NODE_CACHE.invalidateAll(); } /** * Get whether a node is present in the cache * * @param htio * The htio object that contains the node * @param seqNum * The sequence number of the node to check * @return <code>true</code> if the node is present in the cache, * <code>false</code> otherwise */ @VisibleForTesting static <E extends IHTInterval, N extends HTNode<E>> boolean isInCache(HtIo<E, N> htio, int seqNum) { @SuppressWarnings("unchecked") @Nullable HTNode<IHTInterval> present = NODE_CACHE.getIfPresent(new CacheKey((HtIo<IHTInterval, HTNode<IHTInterval>>) htio, seqNum)); return (present != null); } // ------------------------------------------------------------------------ // Instance fields // ------------------------------------------------------------------------ /* Relevant configuration elements from the History Tree */ private final File fStateHistoryFile; private final int fBlockSize; private final int fNodeMaxChildren; private final IHTIntervalReader<E> fObjectReader; private final IHTNodeFactory<E, N> fNodeFactory; /* Fields related to the file I/O */ private final FileInputStream fFileInputStream; private final FileOutputStream fFileOutputStream; private final FileChannel fFileChannelIn; private final FileChannel fFileChannelOut; // ------------------------------------------------------------------------ // Methods // ------------------------------------------------------------------------ /** * Standard constructor * * @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 nodeMaxChildren * The maximum number of children allowed per core (non-leaf) * node. * @param newFile * Flag indicating that the file must be created from scratch * @param intervalReader * The factory to create new tree data elements when reading from * the disk * @param nodeFactory * The factory to create new nodes for this tree * @throws IOException * An exception can be thrown when file cannot be accessed */ public HtIo(File stateHistoryFile, int blockSize, int nodeMaxChildren, boolean newFile, IHTIntervalReader<E> intervalReader, IHTNodeFactory<E, N> nodeFactory) throws IOException { fBlockSize = blockSize; fNodeMaxChildren = nodeMaxChildren; fObjectReader = intervalReader; fNodeFactory = nodeFactory; fStateHistoryFile = stateHistoryFile; if (newFile) { boolean success1 = true; /* Create a new empty History Tree file */ if (fStateHistoryFile.exists()) { success1 = fStateHistoryFile.delete(); } boolean success2 = fStateHistoryFile.createNewFile(); if (!(success1 && success2)) { /* It seems we do not have permission to create the new file */ throw new IOException("Cannot create new file at " + //$NON-NLS-1$ fStateHistoryFile.getName()); } fFileInputStream = new FileInputStream(fStateHistoryFile); fFileOutputStream = new FileOutputStream(fStateHistoryFile, false); } else { /* * We want to open an existing file, make sure we don't squash the * existing content when opening the fos! */ fFileInputStream = new FileInputStream(fStateHistoryFile); fFileOutputStream = new FileOutputStream(fStateHistoryFile, true); } fFileChannelIn = fFileInputStream.getChannel(); fFileChannelOut = fFileOutputStream.getChannel(); } /** * Read a node from the file on disk. * * @param seqNumber * The sequence number of the node to read. * @return The object representing the node * @throws ClosedChannelException * Usually happens because the file was closed while we were * reading. Instead of using a big reader-writer lock, we'll * just catch this exception. */ @SuppressWarnings("unchecked") public N readNode(int seqNumber) throws ClosedChannelException { /* Do a cache lookup. If it's not present it will be loaded from disk */ LOGGER.finest(() -> "[HtIo:CacheLookup] seqNum=" + seqNumber); //$NON-NLS-1$ CacheKey key = new CacheKey((HtIo<IHTInterval, HTNode<IHTInterval>>) this, seqNumber); try { return (N) checkNotNull(NODE_CACHE.get(key)); } catch (ExecutionException e) { /* Get the inner exception that was generated */ Throwable cause = e.getCause(); if (cause instanceof ClosedChannelException) { throw (ClosedChannelException) cause; } /* * Other types of IOExceptions shouldn't happen at this point * though. */ Activator.getInstance().logError(e.getMessage(), e); throw new IllegalStateException(); } } /** * Write the given node to disk. * * @param node * The node to write. */ @SuppressWarnings("unchecked") public void writeNode(N node) { try { int seqNumber = node.getSequenceNumber(); /* "Write-back" the node into the cache */ CacheKey key = new CacheKey((HtIo<IHTInterval, HTNode<IHTInterval>>) this, seqNumber); NODE_CACHE.put(key, (HTNode<IHTInterval>) node); /* Position ourselves at the start of the node and write it */ synchronized (this) { seekFCToNodePos(fFileChannelOut, seqNumber); node.writeSelf(fFileChannelOut); } } catch (IOException e) { /* If we were able to open the file, we should be fine now... */ Activator.getInstance().logError(e.getMessage(), e); } } /** * Get the output file channel, used for writing, positioned after a certain * number of nodes, or at the beginning. * * FIXME: Do not expose the file output. Use rather a method to * writeAtEnd(int nodeOffset, ByteBuffer) * * @param nodeOffset * The offset in the file, in number of nodes. If the value is * lower than 0, the file will be positioned at the beginning. * @return The correctly-seeked input stream */ public FileOutputStream getFileWriter(int nodeOffset) { try { if (nodeOffset < 0) { fFileChannelOut.position(0); } else { seekFCToNodePos(fFileChannelOut, nodeOffset); } } catch (IOException e) { Activator.getInstance().logError(e.getMessage(), e); } return fFileOutputStream; } /** * Retrieve the input stream with which to write the attribute tree. * * FIXME: Do not expose the stream, have a method to write at the end * instead * * @param nodeOffset * The offset in the file, in number of nodes. This should be * after all the nodes. * @return The correctly-seeked input stream */ public FileInputStream supplyATReader(int nodeOffset) { try { /* * Position ourselves at the start of the Mapping section in the * file (which is right after the Blocks) */ seekFCToNodePos(fFileChannelIn, nodeOffset); } catch (IOException e) { Activator.getInstance().logError(e.getMessage(), e); } return fFileInputStream; } /** * Close all file channels and streams. */ public synchronized void closeFile() { try { fFileInputStream.close(); fFileOutputStream.close(); } catch (IOException e) { Activator.getInstance().logError(e.getMessage(), e); } } /** * Delete the history tree file */ public synchronized void deleteFile() { closeFile(); if (!fStateHistoryFile.delete()) { /* We didn't succeed in deleting the file */ Activator.getInstance().logError("Failed to delete" + fStateHistoryFile.getName()); //$NON-NLS-1$ } } /** * Seek the given FileChannel to the position corresponding to the node that * has seqNumber * * @param fc * the channel to seek * @param seqNumber * the node sequence number to seek the channel to * @throws IOException * If some other I/O error occurs */ private void seekFCToNodePos(FileChannel fc, int seqNumber) throws IOException { /* * Cast to (long) is needed to make sure the result is a long too and * doesn't get truncated */ fc.position(IHistoryTree.TREE_HEADER_SIZE + ((long) seqNumber) * fBlockSize); } }