/*******************************************************************************
* Copyright (c) 2013, 2017 Ericsson 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
* Patrick Tasse - Support selection range
* Xavier Raynaud - Support filters tracking
*******************************************************************************/
package org.eclipse.tracecompass.tmf.core.trace;
import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;
import org.apache.commons.io.FileUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.URIUtil;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.tmf.core.TmfCommonConstants;
import org.eclipse.tracecompass.tmf.core.signal.TmfEventFilterAppliedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceClosedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceModelSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceSelectedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfWindowRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
import com.google.common.collect.ImmutableSet;
/**
* Central trace manager for TMF. It tracks the currently opened traces and
* experiment, as well as the currently-selected time or time range and the
* current window time range for each one of those. It also tracks filters
* applied for each trace.
*
* It's a singleton class, so only one instance should exist (available via
* {@link #getInstance()}).
*
* @author Alexandre Montplaisir
*/
@NonNullByDefault
public final class TmfTraceManager {
// ------------------------------------------------------------------------
// Attributes
// ------------------------------------------------------------------------
private final Map<ITmfTrace, TmfTraceContext> fTraces;
/** The currently-selected trace. Should always be part of the trace map */
private @Nullable ITmfTrace fCurrentTrace = null;
private static final String TEMP_DIR_NAME = ".temp"; //$NON-NLS-1$
// ------------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------------
private TmfTraceManager() {
fTraces = new LinkedHashMap<>();
TmfSignalManager.registerVIP(this);
}
/** Singleton instance */
private static @Nullable TmfTraceManager tm = null;
/**
* Get an instance of the trace manager.
*
* @return The trace manager
*/
public static synchronized TmfTraceManager getInstance() {
TmfTraceManager mgr = tm;
if (mgr == null) {
mgr = new TmfTraceManager();
tm = mgr;
}
return mgr;
}
/**
* Disposes the trace manager
*
* @since 2.3
*/
public synchronized void dispose() {
TmfSignalManager.deregister(this);
fTraces.clear();
fCurrentTrace = null;
}
// ------------------------------------------------------------------------
// Accessors
// ------------------------------------------------------------------------
/**
* Get the currently selected trace (normally, the focused editor).
*
* @return The active trace, or <code>null</code> if there is no active
* trace
*/
public synchronized @Nullable ITmfTrace getActiveTrace() {
return fCurrentTrace;
}
/**
* Get the trace set of the currently active trace.
*
* @return The active trace set. Empty (but non-null) if there is no
* currently active trace.
* @see #getTraceSet(ITmfTrace)
*/
public synchronized Collection<ITmfTrace> getActiveTraceSet() {
final ITmfTrace trace = fCurrentTrace;
return getTraceSet(trace);
}
/**
* Get the currently-opened traces, as an unmodifiable set.
*
* @return A set containing the opened traces
*/
public synchronized Set<ITmfTrace> getOpenedTraces() {
return Collections.unmodifiableSet(fTraces.keySet());
}
/**
* Get the editor file for an opened trace.
*
* @param trace
* the trace
* @return the editor file or null if the trace is not opened
*/
public synchronized @Nullable IFile getTraceEditorFile(ITmfTrace trace) {
TmfTraceContext ctx = fTraces.get(trace);
if (ctx != null) {
return ctx.getEditorFile();
}
return null;
}
/**
* Get the {@link TmfTraceContext} of the current active trace. This can be
* used to retrieve the current active/selected time ranges and such.
*
* @return The trace's context.
* @since 1.0
*/
public synchronized TmfTraceContext getCurrentTraceContext() {
TmfTraceContext curCtx = fTraces.get(fCurrentTrace);
if (curCtx == null) {
/* There are no traces opened at the moment. */
return TmfTraceContext.NULL_CONTEXT;
}
return curCtx;
}
/**
* Get the {@link TmfTraceContext} of the given trace.
*
* @param trace
* The trace or experiment.
* @return The trace's context.
* @since 2.3
*/
public synchronized TmfTraceContext getTraceContext(ITmfTrace trace) {
TmfTraceContext curCtx = fTraces.get(trace);
if (curCtx == null) {
/* The trace is not opened. */
return TmfTraceContext.NULL_CONTEXT;
}
return curCtx;
}
// ------------------------------------------------------------------------
// Public utility methods
// ------------------------------------------------------------------------
/**
* Get the trace set of a given trace. For a standard trace, this is simply
* an array with only that trace in it. For experiments, this is an array of
* all the traces contained in this experiment.
*
* @param trace
* The trace or experiment. If it is null, an empty collection
* will be returned.
* @return The corresponding trace set.
*/
public static Collection<ITmfTrace> getTraceSet(@Nullable ITmfTrace trace) {
if (trace == null) {
return ImmutableSet.of();
}
List<@NonNull ITmfTrace> traces = trace.getChildren(ITmfTrace.class);
if (traces.size() > 0) {
return ImmutableSet.copyOf(traces);
}
return ImmutableSet.of(trace);
}
/**
* Get the trace set of a given trace or experiment, including the
* experiment. For a standard trace, this is simply a set containing only
* that trace. For experiments, it is the set of all the traces contained in
* this experiment, along with the experiment.
*
* @param trace
* The trace or experiment. If it is null, an empty collection
* will be returned.
* @return The corresponding trace set, including the experiment.
*/
public static Collection<ITmfTrace> getTraceSetWithExperiment(@Nullable ITmfTrace trace) {
if (trace == null) {
return ImmutableSet.of();
}
if (trace instanceof TmfExperiment) {
TmfExperiment exp = (TmfExperiment) trace;
List<ITmfTrace> traces = exp.getTraces();
Set<ITmfTrace> alltraces = new LinkedHashSet<>(traces);
alltraces.add(exp);
return ImmutableSet.copyOf(alltraces);
}
return Collections.singleton(trace);
}
/**
* Return the path (as a string) to the directory for supplementary files to
* use with a given trace. If no supplementary file directory has been
* configured, a temporary directory based on the trace's name will be
* provided.
*
* @param trace
* The trace
* @return The path to the supplementary file directory (trailing slash is
* INCLUDED!)
*/
public static String getSupplementaryFileDir(ITmfTrace trace) {
IResource resource = trace.getResource();
if (resource == null) {
return getTemporaryDir(trace);
}
String supplDir = null;
try {
supplDir = resource.getPersistentProperty(TmfCommonConstants.TRACE_SUPPLEMENTARY_FOLDER);
} catch (CoreException e) {
return getTemporaryDir(trace);
}
return supplDir + File.separator;
}
/**
* Refresh the supplementary files resources for a trace, so it can pick up
* new files that got created.
*
* @param trace
* The trace for which to refresh the supplementary files
*/
public static void refreshSupplementaryFiles(ITmfTrace trace) {
IResource resource = trace.getResource();
if (resource != null && resource.exists()) {
String supplFolderPath = getSupplementaryFileDir(trace);
IProject project = resource.getProject();
/* Remove the project's path from the supplementary path dir */
if (!supplFolderPath.startsWith(project.getLocation().toOSString())) {
Activator.logWarning(String.format("Supplementary files folder for trace %s is not within the project.", trace.getName())); //$NON-NLS-1$
return;
}
IFolder supplFolder = project.getFolder(supplFolderPath.substring(project.getLocationURI().getPath().length()));
if (supplFolder.exists()) {
try {
supplFolder.refreshLocal(IResource.DEPTH_INFINITE, null);
} catch (CoreException e) {
Activator.logError("Error refreshing resources", e); //$NON-NLS-1$
}
}
}
}
/**
* Delete the supplementary files of a given trace.
*
* @param trace
* The trace for which the supplementary files are to be deleted
* @since 2.2
*/
public static void deleteSupplementaryFiles(ITmfTrace trace) {
try {
FileUtils.cleanDirectory(new File(TmfTraceManager.getSupplementaryFileDir(trace)));
} catch (IOException e) {
Activator.logError("Error deleting supplementary files for trace " + trace.getName(), e); //$NON-NLS-1$
}
refreshSupplementaryFiles(trace);
}
/**
* Update the trace context of a given trace.
*
* @param trace
* The trace
* @param updater
* the function to apply to the trace context's builder
* @since 2.3
*/
public synchronized void updateTraceContext(ITmfTrace trace, UnaryOperator<TmfTraceContext.Builder> updater) {
TmfTraceContext ctx = getTraceContext(trace);
if (!ctx.equals(TmfTraceContext.NULL_CONTEXT)) {
fTraces.put(trace, checkNotNull(updater.apply(ctx.builder())).build());
}
}
// ------------------------------------------------------------------------
// Signal handlers
// ------------------------------------------------------------------------
/**
* Signal handler for the traceOpened signal.
*
* @param signal
* The incoming signal
*/
@TmfSignalHandler
public synchronized void traceOpened(final TmfTraceOpenedSignal signal) {
final ITmfTrace trace = signal.getTrace();
final IFile editorFile = signal.getEditorFile();
final ITmfTimestamp startTs = trace.getStartTime();
long offset = trace.getInitialRangeOffset().toNanos();
long endTime = startTs.toNanos() + offset;
final TmfTimeRange selectionRange = new TmfTimeRange(startTs, startTs);
final TmfTimeRange windowRange = new TmfTimeRange(startTs, TmfTimestamp.fromNanos(endTime));
final TmfTraceContext startCtx = trace.createTraceContext(selectionRange, windowRange, editorFile, null);
fTraces.put(trace, startCtx);
/* We also want to set the newly-opened trace as the active trace */
fCurrentTrace = trace;
}
/**
* Signal propagator
*
* @param signal
* any signal
* @since 2.0
*/
@TmfSignalHandler
public synchronized void signalReceived(final TmfTraceModelSignal signal) {
fTraces.forEach((t, u) -> u.receive(signal));
}
/**
* Handler for the TmfTraceSelectedSignal.
*
* @param signal
* The incoming signal
*/
@TmfSignalHandler
public synchronized void traceSelected(final TmfTraceSelectedSignal signal) {
final ITmfTrace newTrace = signal.getTrace();
if (!fTraces.containsKey(newTrace)) {
throw new RuntimeException();
}
fCurrentTrace = newTrace;
}
/**
* Signal handler for the filterApplied signal.
*
* @param signal
* The incoming signal
*/
@TmfSignalHandler
public synchronized void filterApplied(TmfEventFilterAppliedSignal signal) {
ITmfTrace trace = signal.getTrace();
if (trace == null) {
return;
}
updateTraceContext(trace, builder ->
builder.setFilter(signal.getEventFilter()));
}
/**
* Signal handler for the traceClosed signal.
*
* @param signal
* The incoming signal
*/
@TmfSignalHandler
public synchronized void traceClosed(final TmfTraceClosedSignal signal) {
fTraces.remove(signal.getTrace());
if (fTraces.size() == 0) {
fCurrentTrace = null;
/*
* In other cases, we should receive a traceSelected signal that
* will indicate which trace is the new one.
*/
}
}
/**
* Signal handler for the selection range signal.
*
* The current time of *all* traces whose range contains the requested new
* selection time range will be updated.
*
* @param signal
* The incoming signal
* @since 1.0
*/
@TmfSignalHandler
public synchronized void selectionRangeUpdated(final TmfSelectionRangeUpdatedSignal signal) {
final ITmfTimestamp beginTs = signal.getBeginTime();
final ITmfTimestamp endTs = signal.getEndTime();
for (ITmfTrace trace : fTraces.keySet()) {
if (beginTs.intersects(getValidTimeRange(trace)) || endTs.intersects(getValidTimeRange(trace))) {
updateTraceContext(trace, builder ->
builder.setSelection(new TmfTimeRange(beginTs, endTs)));
}
}
}
/**
* Signal handler for the window range signal.
*
* The current window time range of *all* valid traces will be updated to
* the new requested times.
*
* @param signal
* The incoming signal
* @since 1.0
*/
@TmfSignalHandler
public synchronized void windowRangeUpdated(final TmfWindowRangeUpdatedSignal signal) {
for (ITmfTrace trace : fTraces.keySet()) {
final TmfTimeRange validTr = getValidTimeRange(trace);
if (validTr == null) {
return;
}
/* Determine the new time range */
TmfTimeRange targetTr = signal.getCurrentRange().getIntersection(validTr);
if (targetTr != null) {
updateTraceContext(trace, builder ->
builder.setWindowRange(targetTr));
}
}
}
// ------------------------------------------------------------------------
// Private utility methods
// ------------------------------------------------------------------------
/**
* Return the valid time range of a trace (not the current window time
* range, but the range of all possible valid timestamps).
*
* For a real trace this is the whole range of the trace. For an experiment,
* it goes from the start time of the earliest trace to the end time of the
* latest one.
*
* @param trace
* The trace to check for
* @return The valid time span, or 'null' if the trace is not valid
*/
private @Nullable TmfTimeRange getValidTimeRange(ITmfTrace trace) {
if (!fTraces.containsKey(trace)) {
/* Trace is not part of the currently opened traces */
return null;
}
List<ITmfTrace> traces = trace.getChildren(ITmfTrace.class);
if (traces.isEmpty()) {
/* "trace" is a single trace, return its time range directly */
return trace.getTimeRange();
}
if (traces.size() == 1) {
/* Trace is an experiment with only 1 trace */
return traces.get(0).getTimeRange();
}
/*
* Trace is an trace set with 2+ traces, so get the earliest start and
* the latest end.
*/
ITmfTimestamp start = traces.get(0).getStartTime();
ITmfTimestamp end = traces.get(0).getEndTime();
for (int i = 1; i < traces.size(); i++) {
ITmfTrace curTrace = traces.get(i);
if (curTrace.getStartTime().compareTo(start) < 0) {
start = curTrace.getStartTime();
}
if (curTrace.getEndTime().compareTo(end) > 0) {
end = curTrace.getEndTime();
}
}
return new TmfTimeRange(start, end);
}
/**
* Get the temporary directory path. If there is an instance of Eclipse
* running, the temporary directory will reside under the workspace.
*
* @return the temporary directory path suitable to be passed to the
* java.io.File constructor without a trailing separator
*/
public static String getTemporaryDirPath() {
// Get the workspace path from the properties
String property = System.getProperty("osgi.instance.area"); //$NON-NLS-1$
if (property != null) {
try {
File dir = URIUtil.toFile(URIUtil.fromString(property));
dir = new File(dir.getAbsolutePath() + File.separator + TEMP_DIR_NAME);
if (!dir.exists()) {
dir.mkdirs();
}
return dir.getAbsolutePath();
} catch (URISyntaxException e) {
Activator.logError(e.getLocalizedMessage(), e);
}
}
return System.getProperty("java.io.tmpdir"); //$NON-NLS-1$
}
/**
* Get a temporary directory based on a trace's name. We will create the
* directory if it doesn't exist, so that it's ready to be used.
*/
private static String getTemporaryDir(ITmfTrace trace) {
String pathName = getTemporaryDirPath() +
File.separator +
trace.getName() +
File.separator;
File dir = new File(pathName);
if (!dir.exists()) {
dir.mkdirs();
}
return pathName;
}
}