| 1 | /******************************************************************************* |
| 2 | * Copyright (c) 2012, 2014 Ericsson |
| 3 | * Copyright (c) 2010, 2011 École Polytechnique de Montréal |
| 4 | * Copyright (c) 2010, 2011 Alexandre Montplaisir <alexandre.montplaisir@gmail.com> |
| 5 | * |
| 6 | * All rights reserved. This program and the accompanying materials are |
| 7 | * made available under the terms of the Eclipse Public License v1.0 which |
| 8 | * accompanies this distribution, and is available at |
| 9 | * http://www.eclipse.org/legal/epl-v10.html |
| 10 | * |
| 11 | *******************************************************************************/ |
| 12 | |
| 13 | package org.eclipse.tracecompass.internal.statesystem.core.backend.historytree; |
| 14 | |
| 15 | import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; |
| 16 | |
| 17 | import java.io.File; |
| 18 | import java.io.FileInputStream; |
| 19 | import java.io.FileOutputStream; |
| 20 | import java.io.IOException; |
| 21 | import java.nio.channels.ClosedChannelException; |
| 22 | import java.nio.channels.FileChannel; |
| 23 | import java.util.logging.Logger; |
| 24 | |
| 25 | import org.eclipse.jdt.annotation.NonNull; |
| 26 | import org.eclipse.tracecompass.common.core.log.TraceCompassLog; |
| 27 | import java.util.Objects; |
| 28 | import java.util.concurrent.ExecutionException; |
| 29 | |
| 30 | import org.eclipse.jdt.annotation.Nullable; |
| 31 | import org.eclipse.tracecompass.internal.statesystem.core.Activator; |
| 32 | |
| 33 | import com.google.common.cache.CacheBuilder; |
| 34 | import com.google.common.cache.CacheLoader; |
| 35 | import com.google.common.cache.LoadingCache; |
| 36 | |
| 37 | /** |
| 38 | * This class abstracts inputs/outputs of the HistoryTree nodes. |
| 39 | * |
| 40 | * It contains all the methods and descriptors to handle reading/writing nodes |
| 41 | * to the tree-file on disk and all the caching mechanisms. |
| 42 | * |
| 43 | * This abstraction is mainly for code isolation/clarification purposes. Every |
| 44 | * HistoryTree must contain 1 and only 1 HT_IO element. |
| 45 | * |
| 46 | * @author Alexandre Montplaisir |
| 47 | */ |
| 48 | class HT_IO { |
| 49 | |
| 50 | private static final Logger LOGGER = TraceCompassLog.getLogger(HT_IO.class); |
| 51 | |
| 52 | // ------------------------------------------------------------------------ |
| 53 | // Global cache of nodes |
| 54 | // ------------------------------------------------------------------------ |
| 55 | |
| 56 | private static final class CacheKey { |
| 57 | |
| 58 | public final HT_IO fStateHistory; |
| 59 | public final int fSeqNumber; |
| 60 | |
| 61 | public CacheKey(HT_IO stateHistory, int seqNumber) { |
| 62 | fStateHistory = stateHistory; |
| 63 | fSeqNumber = seqNumber; |
| 64 | } |
| 65 | |
| 66 | @Override |
| 67 | public int hashCode() { |
| 68 | return Objects.hash(fStateHistory, fSeqNumber); |
| 69 | } |
| 70 | |
| 71 | @Override |
| 72 | public boolean equals(@Nullable Object obj) { |
| 73 | if (this == obj) { |
| 74 | return true; |
| 75 | } |
| 76 | if (obj == null) { |
| 77 | return false; |
| 78 | } |
| 79 | if (getClass() != obj.getClass()) { |
| 80 | return false; |
| 81 | } |
| 82 | CacheKey other = (CacheKey) obj; |
| 83 | return (fStateHistory.equals(other.fStateHistory) && |
| 84 | fSeqNumber == other.fSeqNumber); |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | private static final int CACHE_SIZE = 256; |
| 89 | |
| 90 | private static final LoadingCache<CacheKey, HTNode> NODE_CACHE = |
| 91 | checkNotNull(CacheBuilder.newBuilder() |
| 92 | .maximumSize(CACHE_SIZE) |
| 93 | .build(new CacheLoader<CacheKey, HTNode>() { |
| 94 | @Override |
| 95 | public HTNode load(CacheKey key) throws IOException { |
| 96 | HT_IO io = key.fStateHistory; |
| 97 | int seqNb = key.fSeqNumber; |
| 98 | |
| 99 | LOGGER.finest(() -> "[HtIo:CacheMiss] seqNum=" + seqNb); //$NON-NLS-1$ |
| 100 | |
| 101 | synchronized (io) { |
| 102 | io.seekFCToNodePos(io.fFileChannelIn, seqNb); |
| 103 | return HTNode.readNode(io.fConfig, io.fFileChannelIn); |
| 104 | } |
| 105 | } |
| 106 | })); |
| 107 | |
| 108 | |
| 109 | // ------------------------------------------------------------------------ |
| 110 | // Instance fields |
| 111 | // ------------------------------------------------------------------------ |
| 112 | |
| 113 | /* Configuration of the History Tree */ |
| 114 | private final HTConfig fConfig; |
| 115 | |
| 116 | /* Fields related to the file I/O */ |
| 117 | private final FileInputStream fFileInputStream; |
| 118 | private final FileOutputStream fFileOutputStream; |
| 119 | private final FileChannel fFileChannelIn; |
| 120 | private final FileChannel fFileChannelOut; |
| 121 | |
| 122 | // ------------------------------------------------------------------------ |
| 123 | // Methods |
| 124 | // ------------------------------------------------------------------------ |
| 125 | |
| 126 | |
| 127 | |
| 128 | /** |
| 129 | * Standard constructor |
| 130 | * |
| 131 | * @param config |
| 132 | * The configuration object for the StateHistoryTree |
| 133 | * @param newFile |
| 134 | * Flag indicating that the file must be created from scratch |
| 135 | * |
| 136 | * @throws IOException |
| 137 | * An exception can be thrown when file cannot be accessed |
| 138 | */ |
| 139 | public HT_IO(HTConfig config, boolean newFile) throws IOException { |
| 140 | fConfig = config; |
| 141 | |
| 142 | File historyTreeFile = config.getStateFile(); |
| 143 | if (newFile) { |
| 144 | boolean success1 = true; |
| 145 | /* Create a new empty History Tree file */ |
| 146 | if (historyTreeFile.exists()) { |
| 147 | success1 = historyTreeFile.delete(); |
| 148 | } |
| 149 | boolean success2 = historyTreeFile.createNewFile(); |
| 150 | if (!(success1 && success2)) { |
| 151 | /* It seems we do not have permission to create the new file */ |
| 152 | throw new IOException("Cannot create new file at " + //$NON-NLS-1$ |
| 153 | historyTreeFile.getName()); |
| 154 | } |
| 155 | fFileInputStream = new FileInputStream(historyTreeFile); |
| 156 | fFileOutputStream = new FileOutputStream(historyTreeFile, false); |
| 157 | } else { |
| 158 | /* |
| 159 | * We want to open an existing file, make sure we don't squash the |
| 160 | * existing content when opening the fos! |
| 161 | */ |
| 162 | fFileInputStream = new FileInputStream(historyTreeFile); |
| 163 | fFileOutputStream = new FileOutputStream(historyTreeFile, true); |
| 164 | } |
| 165 | fFileChannelIn = fFileInputStream.getChannel(); |
| 166 | fFileChannelOut = fFileOutputStream.getChannel(); |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * Read a node from the file on disk. |
| 171 | * |
| 172 | * @param seqNumber |
| 173 | * The sequence number of the node to read. |
| 174 | * @return The object representing the node |
| 175 | * @throws ClosedChannelException |
| 176 | * Usually happens because the file was closed while we were |
| 177 | * reading. Instead of using a big reader-writer lock, we'll |
| 178 | * just catch this exception. |
| 179 | */ |
| 180 | public @NonNull HTNode readNode(int seqNumber) throws ClosedChannelException { |
| 181 | /* Do a cache lookup. If it's not present it will be loaded from disk */ |
| 182 | LOGGER.finest(() -> "[HtIo:CacheLookup] seqNum=" + seqNumber); //$NON-NLS-1$ |
| 183 | CacheKey key = new CacheKey(this, seqNumber); |
| 184 | try { |
| 185 | return checkNotNull(NODE_CACHE.get(key)); |
| 186 | |
| 187 | } catch (ExecutionException e) { |
| 188 | /* Get the inner exception that was generated */ |
| 189 | Throwable cause = e.getCause(); |
| 190 | if (cause instanceof ClosedChannelException) { |
| 191 | throw (ClosedChannelException) cause; |
| 192 | } |
| 193 | /* |
| 194 | * Other types of IOExceptions shouldn't happen at this point though. |
| 195 | */ |
| 196 | Activator.getDefault().logError(e.getMessage(), e); |
| 197 | throw new IllegalStateException(); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | public void writeNode(HTNode node) { |
| 202 | try { |
| 203 | int seqNumber = node.getSequenceNumber(); |
| 204 | |
| 205 | /* "Write-back" the node into the cache */ |
| 206 | CacheKey key = new CacheKey(this, seqNumber); |
| 207 | NODE_CACHE.put(key, node); |
| 208 | |
| 209 | /* Position ourselves at the start of the node and write it */ |
| 210 | synchronized (this) { |
| 211 | seekFCToNodePos(fFileChannelOut, seqNumber); |
| 212 | node.writeSelf(fFileChannelOut); |
| 213 | } |
| 214 | } catch (IOException e) { |
| 215 | /* If we were able to open the file, we should be fine now... */ |
| 216 | Activator.getDefault().logError(e.getMessage(), e); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | public FileChannel getFcOut() { |
| 221 | return fFileChannelOut; |
| 222 | } |
| 223 | |
| 224 | public FileInputStream supplyATReader(int nodeOffset) { |
| 225 | try { |
| 226 | /* |
| 227 | * Position ourselves at the start of the Mapping section in the |
| 228 | * file (which is right after the Blocks) |
| 229 | */ |
| 230 | seekFCToNodePos(fFileChannelIn, nodeOffset); |
| 231 | } catch (IOException e) { |
| 232 | Activator.getDefault().logError(e.getMessage(), e); |
| 233 | } |
| 234 | return fFileInputStream; |
| 235 | } |
| 236 | |
| 237 | public synchronized void closeFile() { |
| 238 | try { |
| 239 | fFileInputStream.close(); |
| 240 | fFileOutputStream.close(); |
| 241 | } catch (IOException e) { |
| 242 | Activator.getDefault().logError(e.getMessage(), e); |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | public synchronized void deleteFile() { |
| 247 | closeFile(); |
| 248 | |
| 249 | File historyTreeFile = fConfig.getStateFile(); |
| 250 | if (!historyTreeFile.delete()) { |
| 251 | /* We didn't succeed in deleting the file */ |
| 252 | Activator.getDefault().logError("Failed to delete" + historyTreeFile.getName()); //$NON-NLS-1$ |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Seek the given FileChannel to the position corresponding to the node that |
| 258 | * has seqNumber |
| 259 | * |
| 260 | * @param fc |
| 261 | * the channel to seek |
| 262 | * @param seqNumber |
| 263 | * the node sequence number to seek the channel to |
| 264 | * @throws IOException |
| 265 | * If some other I/O error occurs |
| 266 | */ |
| 267 | private void seekFCToNodePos(FileChannel fc, int seqNumber) |
| 268 | throws IOException { |
| 269 | /* |
| 270 | * Cast to (long) is needed to make sure the result is a long too and |
| 271 | * doesn't get truncated |
| 272 | */ |
| 273 | fc.position(HistoryTree.TREE_HEADER_SIZE |
| 274 | + ((long) seqNumber) * fConfig.getBlockSize()); |
| 275 | } |
| 276 | |
| 277 | } |