| /******************************************************************************* |
| * Copyright (c) 2013, 2017 Ericsson |
| * |
| * 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: |
| * Marc-Andre Laperle - Initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.tmf.core.trace.indexer; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.RandomAccessFile; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.FileChannel; |
| import java.nio.file.Files; |
| import java.text.MessageFormat; |
| import java.util.Objects; |
| |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.tracecompass.internal.tmf.core.Activator; |
| import org.eclipse.tracecompass.internal.tmf.core.TmfCoreTracer; |
| import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange; |
| import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp; |
| import org.eclipse.tracecompass.tmf.core.trace.indexer.ITmfPersistentlyIndexable; |
| |
| /** |
| * Common implementation of file-based checkpoint collection |
| * |
| * @author Marc-Andre Laperle |
| */ |
| public abstract class AbstractFileCheckpointCollection implements ICheckpointCollection { |
| |
| private static final int INVALID_VERSION = -1; |
| private static final int VERSION = 3; |
| private static final int SUB_VERSION_NONE = -1; |
| |
| /** |
| * The base file header, can be extended |
| */ |
| protected class CheckpointCollectionFileHeader { |
| private static final int SIZE = INT_SIZE + |
| INT_SIZE + |
| LONG_SIZE + |
| LONG_SIZE + |
| MAX_TIME_RANGE_SERIALIZE_SIZE; |
| |
| /** |
| * Get the size of the header in bytes. This should be overridden if the |
| * header is augmented with more data |
| * |
| * @return the size of the header in bytes |
| */ |
| public int getSize() { |
| return SIZE; |
| } |
| |
| /** |
| * Get the sub version of this header |
| * |
| * @return the sub version |
| */ |
| public int getSubVersion() { |
| return SUB_VERSION_NONE; |
| } |
| |
| /** |
| * Constructs a new file header for an existing file |
| * |
| * @param randomAccessFile |
| * the existing file |
| * @throws IOException |
| * if an I/O error occurs reading from the file |
| */ |
| public CheckpointCollectionFileHeader(RandomAccessFile randomAccessFile) throws IOException { |
| fVersion = randomAccessFile.readInt(); |
| fSize = randomAccessFile.readInt(); |
| fNbEvents = randomAccessFile.readLong(); |
| ByteBuffer b = ByteBuffer.allocate(MAX_TIME_RANGE_SERIALIZE_SIZE); |
| b.clear(); |
| fFileChannel.read(b); |
| b.flip(); |
| fTimeRange = new TmfTimeRange(TmfTimestamp.create(b), TmfTimestamp.create(b)); |
| } |
| |
| /** |
| * Constructs a new file header for the given version |
| * |
| * @param version |
| * the version |
| */ |
| public CheckpointCollectionFileHeader(int version) { |
| fVersion = version; |
| } |
| |
| /** |
| * Serialize the header to a file |
| * |
| * @param randomAccessFile |
| * the existing file |
| * @throws IOException |
| * if an I/O error occurs writing to the file |
| */ |
| public void serialize(RandomAccessFile randomAccessFile) throws IOException { |
| randomAccessFile.seek(0); |
| // Version is written at the very last on dispose |
| randomAccessFile.writeInt(INVALID_VERSION); |
| randomAccessFile.writeInt(fSize); |
| randomAccessFile.writeLong(fNbEvents); |
| |
| ByteBuffer b = ByteBuffer.allocate(MAX_TIME_RANGE_SERIALIZE_SIZE); |
| b.clear(); |
| TmfTimestamp.serialize(b, fTimeRange.getStartTime()); |
| TmfTimestamp.serialize(b, fTimeRange.getEndTime()); |
| b.rewind(); |
| fFileChannel.write(b); |
| } |
| |
| /** |
| * The version of the collection. Should be incremented if a binary |
| * incompatible change occurs. |
| */ |
| protected final int fVersion; |
| /** |
| * The size of the collection expressed in a number of checkpoints. |
| */ |
| protected int fSize = 0; |
| /** |
| * The total number of events in the trace |
| */ |
| protected long fNbEvents; |
| |
| /** |
| * The time range of the trace. |
| */ |
| protected TmfTimeRange fTimeRange = new TmfTimeRange(TmfTimestamp.ZERO, TmfTimestamp.ZERO); |
| } |
| |
| /** |
| * The size of an int in bytes |
| */ |
| protected static final int INT_SIZE = 4; |
| /** |
| * The size of a long in bytes |
| */ |
| protected static final int LONG_SIZE = 8; |
| |
| /** |
| * The maximum size of the serialize buffer when writing the time range |
| */ |
| protected static final int MAX_TIME_RANGE_SERIALIZE_SIZE = 128; |
| |
| /** |
| * The originating trace |
| */ |
| private ITmfPersistentlyIndexable fTrace; |
| |
| private long fCacheMisses = 0; |
| private boolean fCreatedFromScratch; |
| private boolean fIsDirty = false; |
| |
| /** |
| * File handle for the file being read/written |
| */ |
| private @Nullable RandomAccessFile fRandomAccessFile; |
| /** |
| * File handle for the file being read/written |
| */ |
| private File fFile; |
| |
| /** |
| * The base file header |
| */ |
| private final CheckpointCollectionFileHeader fHeader; |
| |
| // Cached values |
| private FileChannel fFileChannel; |
| |
| /** |
| * Constructs a checkpoint collection for a given trace from scratch or from |
| * an existing file. When the checkpoint collection is created from scratch, |
| * it is populated by subsequent calls to {@link #insert}. |
| * |
| * @param file |
| * the file to use as the persistent storage |
| * @param trace |
| * the trace |
| */ |
| public AbstractFileCheckpointCollection(File file, ITmfPersistentlyIndexable trace) { |
| fTrace = trace; |
| fFile = file; |
| setCreatedFromScratch(!fFile.exists()); |
| |
| CheckpointCollectionFileHeader header = null; |
| |
| if (!isCreatedFromScratch()) { |
| header = tryRestore(); |
| if (header == null) { |
| delete(); |
| } |
| } |
| |
| /* delete above may have reset the value */ |
| if (isCreatedFromScratch()) { |
| fIsDirty = true; |
| header = initialize(); |
| } |
| |
| fHeader = header; |
| } |
| |
| /** |
| * Creates a new basic file header with the version field initialized. This |
| * should be overridden if the file header is extended |
| * |
| * @return the created file header |
| */ |
| protected CheckpointCollectionFileHeader createHeader() { |
| return new CheckpointCollectionFileHeader(VERSION); |
| } |
| |
| /** |
| * Creates a new basic file header for an existing file. This should be |
| * overridden if the file header is extended |
| * |
| * @param randomAccessFile |
| * the existing file |
| * @return the created file header |
| * @throws IOException |
| * if an I/O error occurs reading from the file |
| */ |
| protected CheckpointCollectionFileHeader createHeader(RandomAccessFile randomAccessFile) throws IOException { |
| return new CheckpointCollectionFileHeader(randomAccessFile); |
| } |
| |
| /** |
| * Get the version of the collection. |
| * |
| * @return the version of the collection. |
| */ |
| protected int getVersion() { |
| return VERSION; |
| } |
| |
| /** |
| * Get the sub version of the collection. |
| * |
| * @return the sub version of the collection. |
| */ |
| protected int getSubVersion() { |
| return SUB_VERSION_NONE; |
| } |
| |
| private CheckpointCollectionFileHeader initialize() { |
| CheckpointCollectionFileHeader header = null; |
| try { |
| RandomAccessFile randomAccessFile = new RandomAccessFile(fFile, "rw"); //$NON-NLS-1$ |
| fFileChannel = randomAccessFile.getChannel(); |
| header = createHeader(); |
| |
| // Reserve space for header |
| randomAccessFile.setLength(header.getSize()); |
| TmfCoreTracer.traceIndexer(CheckpointCollectionFileHeader.class.getSimpleName() + " initialize " + "nbEvents: " + header.fNbEvents + " fTimeRange: " + header.fTimeRange); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| fRandomAccessFile = randomAccessFile; |
| } catch (IOException e) { |
| Activator.logError(MessageFormat.format(Messages.ErrorOpeningIndex, fFile), e); |
| return null; |
| } |
| |
| return header; |
| } |
| |
| /** |
| * Try to restore the index from disk. Try to open the file and check the |
| * version. Returns the loaded header or null if it could not be loaded. |
| * |
| * @return the loaded header or null if it could not be loaded. |
| */ |
| private CheckpointCollectionFileHeader tryRestore() { |
| CheckpointCollectionFileHeader header = null; |
| |
| RandomAccessFile randomAccessFile = fRandomAccessFile; |
| try { |
| randomAccessFile = new RandomAccessFile(fFile, "rw"); //$NON-NLS-1$ |
| fFileChannel = randomAccessFile.getChannel(); |
| } catch (FileNotFoundException e) { |
| Activator.logError(MessageFormat.format(Messages.ErrorOpeningIndex, fFile), e); |
| return null; |
| } finally { |
| fRandomAccessFile = randomAccessFile; |
| } |
| |
| try { |
| header = createHeader(randomAccessFile); |
| if (header.fVersion != VERSION || header.getSubVersion() != getSubVersion()) { |
| return null; |
| } |
| TmfCoreTracer.traceIndexer(CheckpointCollectionFileHeader.class.getSimpleName() + " read " + fFile + " nbEvents: " + header.fNbEvents + " fTimeRange: " + header.fTimeRange); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } catch (IOException e) { |
| Activator.logError(MessageFormat.format(Messages.IOErrorReadingHeader, fFile), e); |
| return null; |
| } finally { |
| fRandomAccessFile = randomAccessFile; |
| } |
| |
| |
| return header; |
| } |
| |
| /** |
| * Marks the index as dirty. Writes an invalid file header, and sets the dirty |
| * flag which will cause the new file header to be serialized when the index is |
| * disposed. |
| */ |
| protected void markDirty() { |
| RandomAccessFile randomAccessFile = fRandomAccessFile; |
| if (fIsDirty || randomAccessFile == null) { |
| return; |
| } |
| try { |
| // Write an invalid version until the very last moment when dispose |
| // the index. This is how we know if it's corrupted or not. |
| randomAccessFile.seek(0); |
| randomAccessFile.writeInt(INVALID_VERSION); |
| } catch (IOException e) { |
| Activator.logError(MessageFormat.format(Messages.IOErrorWritingHeader, fFile), e); |
| } |
| fIsDirty = true; |
| } |
| |
| /** |
| * |
| * @return true if the checkpoint collection was created from scratch, false |
| * otherwise |
| */ |
| @Override |
| public boolean isCreatedFromScratch() { |
| return fCreatedFromScratch; |
| } |
| |
| /** |
| * Set whether or not the collection is created from scratch |
| * |
| * @param isCreatedFromScratch |
| * whether or not the collection is created from scratch |
| */ |
| protected void setCreatedFromScratch(boolean isCreatedFromScratch) { |
| fCreatedFromScratch = isCreatedFromScratch; |
| } |
| |
| /** |
| * @return the number of cache misses. |
| */ |
| public long getCacheMisses() { |
| return fCacheMisses; |
| } |
| |
| /** |
| * Increment the number of cache misses. |
| */ |
| protected void incCacheMisses() { |
| ++fCacheMisses; |
| } |
| |
| /** |
| * Returns the size of the checkpoint collection expressed as a number of |
| * checkpoints. |
| * |
| * @return the size of the checkpoint collection |
| */ |
| @Override |
| public int size() { |
| return fHeader.fSize; |
| } |
| |
| /** |
| * Set the trace time range |
| * |
| * @param timeRange |
| * the trace time range |
| */ |
| @Override |
| public void setTimeRange(TmfTimeRange timeRange) { |
| if (!fHeader.fTimeRange.equals(timeRange)) { |
| markDirty(); |
| } |
| fHeader.fTimeRange = timeRange; |
| } |
| |
| /** |
| * Get the trace time range |
| * |
| * @return the trace time range |
| */ |
| @Override |
| public TmfTimeRange getTimeRange() { |
| return fHeader.fTimeRange; |
| } |
| |
| /** |
| * Set the number of events in the trace |
| * |
| * @param nbEvents |
| * the number of events in the trace |
| */ |
| @Override |
| public void setNbEvents(long nbEvents) { |
| if (fHeader.fNbEvents != nbEvents) { |
| markDirty(); |
| } |
| fHeader.fNbEvents = nbEvents; |
| } |
| |
| /** |
| * Get the number of events in the trace |
| * |
| * @return the number of events in the trace |
| */ |
| @Override |
| public long getNbEvents() { |
| return fHeader.fNbEvents; |
| } |
| |
| /** |
| * Get the trace |
| * |
| * @return the trace |
| */ |
| protected ITmfPersistentlyIndexable getTrace() { |
| return fTrace; |
| } |
| |
| /** |
| * Get the random access file currently opened |
| * |
| * @return the file |
| */ |
| protected RandomAccessFile getRandomAccessFile() { |
| return fRandomAccessFile; |
| } |
| |
| /** |
| * Get the file channel currently used for the index |
| * |
| * @return the file channel |
| * @deprecated potential resource leak, use {@link #getRandomAccessFile()} instead |
| */ |
| @Deprecated |
| protected FileChannel getFileChannel() { |
| return Objects.requireNonNull(fRandomAccessFile).getChannel(); |
| } |
| |
| /** |
| * Get the file handle for the index |
| * |
| * @return the file |
| */ |
| protected File getFile() { |
| return fFile; |
| } |
| |
| /** |
| * Get the header for this collection |
| * |
| * @return the header |
| */ |
| public CheckpointCollectionFileHeader getHeader() { |
| return fHeader; |
| } |
| |
| /** |
| * Dispose and delete the checkpoint collection |
| */ |
| @Override |
| public void delete() { |
| dispose(true); |
| File file = fFile; |
| if (file.exists()) { |
| try { |
| Files.delete(file.toPath()); |
| } catch (IOException e) { |
| Activator.logError(e.getMessage(), e); |
| } |
| } |
| } |
| |
| /** |
| * Dispose the collection and its resources |
| */ |
| @Override |
| public void dispose() { |
| dispose(false); |
| } |
| |
| private void dispose(boolean deleting) { |
| try { |
| RandomAccessFile randomAccessFile = fRandomAccessFile; |
| if (randomAccessFile != null) { |
| if (!deleting && fIsDirty) { |
| if (fHeader != null) { |
| fHeader.serialize(randomAccessFile); |
| } |
| |
| randomAccessFile.seek(0); |
| randomAccessFile.writeInt(getVersion()); |
| fIsDirty = false; |
| } |
| |
| randomAccessFile.close(); |
| } |
| setCreatedFromScratch(true); |
| String headerTrace = fHeader == null ? "No header" : "nbEvents: " + fHeader.fNbEvents + " timerange:" + fHeader.fTimeRange; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| TmfCoreTracer.traceIndexer(this.getClass().getSimpleName() + " disposed. " + headerTrace); //$NON-NLS-1$ |
| } catch (IOException e) { |
| Activator.logError(MessageFormat.format(Messages.IOErrorClosingIndex, fFile), e); |
| } finally { |
| fRandomAccessFile = null; |
| } |
| } |
| } |