| /******************************************************************************* |
| * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir |
| * |
| * 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 |
| *******************************************************************************/ |
| |
| package org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module; |
| |
| import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; |
| import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNullContents; |
| import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; |
| |
| import java.io.File; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.WeakHashMap; |
| import java.util.function.Predicate; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.regex.Pattern; |
| import java.util.stream.Stream; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| import org.eclipse.tracecompass.common.core.log.TraceCompassLog; |
| import org.eclipse.tracecompass.common.core.log.TraceCompassLogUtils; |
| import org.eclipse.tracecompass.common.core.process.ProcessUtils; |
| import org.eclipse.tracecompass.common.core.process.ProcessUtils.OutputReaderFunction; |
| import org.eclipse.tracecompass.internal.analysis.lami.core.Activator; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.LamiStrings; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.ShellUtils; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiDurationAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiEmptyAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiGenericAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNameAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQNumberAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiIRQTypeAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiMixedAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessNameAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessPIDAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiProcessTIDAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeBeginAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeDurationAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimeRangeEndAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTimestampAspect; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiData; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiData.DataType; |
| import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiTimeRange; |
| import org.eclipse.tracecompass.tmf.core.analysis.ondemand.IOnDemandAnalysis; |
| import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange; |
| import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace; |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableMultimap; |
| import com.google.common.collect.Multimap; |
| |
| /** |
| * Base class for analysis modules that call external scripts implementing the |
| * LAMI protocol. |
| * |
| * @author Alexandre Montplaisir |
| * @author Philippe Proulx |
| */ |
| public class LamiAnalysis implements IOnDemandAnalysis { |
| |
| private static final Logger LOGGER = TraceCompassLog.getLogger(LamiAnalysis.class); |
| private static final String DOUBLE_QUOTES = "\""; //$NON-NLS-1$ |
| |
| /* Flags passed to the analysis scripts */ |
| private static final String MI_VERSION_FLAG = "--mi-version"; //$NON-NLS-1$ |
| private static final String TEST_COMPATIBILITY_FLAG = "--test-compatibility"; //$NON-NLS-1$ |
| private static final String METADATA_FLAG = "--metadata"; //$NON-NLS-1$ |
| private static final String PROGRESS_FLAG = "--output-progress"; //$NON-NLS-1$ |
| private static final String BEGIN_FLAG = "--begin"; //$NON-NLS-1$ |
| private static final String END_FLAG = "--end"; //$NON-NLS-1$ |
| |
| /* Log messages */ |
| private static final String LOG_VERSION = "LamiAnalysis:MIVersionReport"; //$NON-NLS-1$ |
| private static final String VERSION = "version"; //$NON-NLS-1$ |
| private static final String INVALID_MI_VERSION = "No specific or invalid MI version"; //$NON-NLS-1$ |
| private static final String LOG_NO_MI_VERSION = "LamiAnalysis:InvalidMIVersionReport"; //$NON-NLS-1$ |
| private static final String LOG_RUNNING_MESSAGE = "LamiAnalysis:RunningCommand"; //$NON-NLS-1$ |
| private static final String ERROR_PARSING_METADATA = "LamiAnalysis:ErrorParsingMetadata"; //$NON-NLS-1$ |
| private static final String RUNNING_METADATA_COMMAND = "LamiAnalysis:RunningMetadataCommand"; //$NON-NLS-1$ |
| private static final String COMMAND = "command"; //$NON-NLS-1$ |
| private static final String ERROR_PARSING_EXECUTION_OUTPUT = "LamiAnalysis:ErrorParsingExecutionOutput"; //$NON-NLS-1$ |
| private static final String RUNNING_EXECUTE_COMMAND = "LamiAnalysis:RunningExecuteCommand"; //$NON-NLS-1$ |
| |
| /* Tokens of the complete command */ |
| private final List<String> fScriptCommand; |
| |
| /* 0 means pre-1.0 LAMI protocol */ |
| private int fMiVersion = 0; |
| |
| /* Custom name given to this analysis. */ |
| private final String fName; |
| |
| /* |
| * True if this analysis is defined by a user, that is, not |
| * provided by TC. |
| */ |
| private final boolean fIsUserDefined; |
| |
| /* |
| * The predicate to use to determine if a given trace applies |
| * to this analysis, that is, the given trace's type is compatible |
| * with this analysis a priori. The analysis might not be able to |
| * execute for a given trace, but this is returned by canExecute(). |
| */ |
| private final Predicate<ITmfTrace> fAppliesTo; |
| |
| /* Data defined by the analysis's metadata */ |
| private @Nullable String fAnalysisTitle; |
| private @Nullable Map<String, LamiTableClass> fTableClasses; |
| |
| /* Cache: true if the initialization process took place already. */ |
| private boolean fInitialized = false; |
| |
| /* |
| * Cache: assigns a boolean to a trace which indicates if this analysis can |
| * be executed on the given trace. Presence in the map indicates the |
| * compatibility test has already run, so "false" is different than "absent" |
| * here. |
| * |
| * Uses weak keys, so we do not hold references to trace objects and prevent |
| * them from being disposed. |
| */ |
| private final Map<ITmfTrace, Boolean> fTraceCompatibilityCache = new WeakHashMap<>(); |
| |
| /** |
| * Available features. |
| */ |
| private enum Features { |
| /** The MI version of the analysis is supported. */ |
| VERSION_IS_SUPPORTED, |
| |
| /** The analysis is supported at all. */ |
| SUPPORTED, |
| |
| /** The analysis supports a progress indication output. */ |
| OUTPUT_PROGRESS, |
| |
| /** The analysis supports testing a given trace for compatibility. */ |
| TEST_COMPATIBILITY |
| } |
| |
| /* Available features depending on the MI version */ |
| private final EnumSet<Features> fFeatures = checkNotNull(EnumSet.noneOf(Features.class)); |
| |
| /** |
| * Constructor. To be called by implementing classes. |
| * |
| * @param name |
| * Name of this analysis |
| * @param isUserDefined |
| * {@code true} if this is a user-defined analysis |
| * @param appliesTo |
| * Predicate to use to check whether or not this analysis applies |
| * to a given trace |
| * @param args |
| * Analysis arguments, including the executable name (first |
| * argument) |
| */ |
| public LamiAnalysis(String name, boolean isUserDefined, Predicate<ITmfTrace> appliesTo, |
| List<String> args) { |
| fScriptCommand = ImmutableList.copyOf(args); |
| fName = name; |
| fIsUserDefined = isUserDefined; |
| fAppliesTo = appliesTo; |
| } |
| |
| /** |
| * Map of pre-defined charts, for every table class names. |
| * |
| * If a table class is not in this map then it means that table has no |
| * predefined charts. |
| * |
| * @return The chart models, per table class names |
| */ |
| protected Multimap<String, LamiChartModel> getPredefinedCharts() { |
| return ImmutableMultimap.of(); |
| } |
| |
| @Override |
| public final boolean appliesTo(ITmfTrace trace) { |
| return fAppliesTo.test(trace); |
| } |
| |
| private boolean testCompatibility(ITmfTrace trace) { |
| final @NonNull String tracePath = checkNotNull(trace.getPath()); |
| |
| final List<String> commandLine = ImmutableList.<@NonNull String> builder() |
| .addAll(fScriptCommand) |
| .add(TEST_COMPATIBILITY_FLAG) |
| .add(tracePath) |
| .build(); |
| final boolean isCompatible = (getOutputFromCommand(commandLine) != null); |
| |
| /* Add this result to the compatibility cache. */ |
| fTraceCompatibilityCache.put(trace, isCompatible); |
| |
| return isCompatible; |
| } |
| |
| private boolean isSupported() { |
| initialize(); |
| |
| return fFeatures.contains(Features.SUPPORTED); |
| } |
| |
| @Override |
| public boolean canExecute(ITmfTrace trace) { |
| /* Make sure this analysis is supported at all. */ |
| if (!isSupported()) { |
| return false; |
| } |
| |
| if (!fFeatures.contains(Features.TEST_COMPATIBILITY)) { |
| /* |
| * No support for dynamic compatibility testing: suppose this |
| * analysis can run on any trace. |
| */ |
| return true; |
| } |
| |
| /* Check if this trace is already registered in the cache. */ |
| if (fTraceCompatibilityCache.getOrDefault(trace, false)) { |
| return true; |
| } |
| |
| /* |
| * Test compatibility since it's supported. |
| */ |
| return testCompatibility(trace); |
| } |
| |
| private void setFeatures() { |
| if (fMiVersion == 0) { |
| // Pre-1.0 LAMI protocol: supported for backward compatibility |
| fFeatures.add(Features.VERSION_IS_SUPPORTED); |
| return; |
| } |
| |
| if (fMiVersion >= 100 && fMiVersion < 200) { |
| // LAMI 1.x |
| fFeatures.add(Features.VERSION_IS_SUPPORTED); |
| fFeatures.add(Features.OUTPUT_PROGRESS); |
| fFeatures.add(Features.TEST_COMPATIBILITY); |
| } |
| } |
| |
| private void readVersion() { |
| final String command = fScriptCommand.get(0); |
| final List<String> commandLine = ImmutableList.<@NonNull String> builder() |
| .add(command).add(MI_VERSION_FLAG).build(); |
| final String output = getOutputFromCommand(commandLine); |
| |
| if (output == null) { |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.INFO, LOG_NO_MI_VERSION, INVALID_MI_VERSION, command); |
| return; |
| } |
| |
| final String versionString = output.trim(); |
| |
| if (!versionString.matches("\\d{1,3}\\.\\d{1,3}")) { //$NON-NLS-1$ |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.INFO, LOG_NO_MI_VERSION, COMMAND, command, VERSION, versionString); |
| return; |
| } |
| |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.FINE, LOG_VERSION, COMMAND, command, VERSION, versionString); |
| |
| |
| final String[] parts = versionString.split("\\."); //$NON-NLS-1$ |
| final int major = Integer.valueOf(parts[0]); |
| final int minor = Integer.valueOf(parts[1]); |
| |
| fMiVersion = major * 100 + minor; |
| } |
| |
| private static boolean executableExists(String name) { |
| if (name.contains(File.separator)) { |
| /* This seems like a path, not just an executable name */ |
| return Files.isExecutable(Paths.get(name)); |
| } |
| |
| /* Check if this name is found in the PATH environment variable */ |
| final String pathEnv = System.getenv("PATH"); //$NON-NLS-1$ |
| final String[] exeDirs = pathEnv.split(checkNotNull(Pattern.quote(File.pathSeparator))); |
| |
| return Stream.of(exeDirs) |
| .map(Paths::get) |
| .anyMatch(path -> Files.isExecutable(path.resolve(name))); |
| } |
| |
| /** |
| * Perform initialization of the LAMI script. This means verifying that it |
| * is actually present on disk, and that it returns correct --metadata. |
| */ |
| @VisibleForTesting |
| protected synchronized void initialize() { |
| if (fInitialized) { |
| /* Already initialized */ |
| return; |
| } |
| |
| fInitialized = true; |
| |
| /* Step 1: Check if the script's expected executable is on the PATH. */ |
| final String executable = fScriptCommand.get(0); |
| final boolean executableExists = executableExists(executable); |
| |
| if (!executableExists) { |
| /* Script is not found */ |
| return; |
| } |
| |
| /* |
| * Step 2: Read the version, which also gives us an indication as |
| * to whether or not this executable supports LAMI 1.0. |
| */ |
| readVersion(); |
| |
| /* |
| * Set the available features according to the MI version. |
| */ |
| setFeatures(); |
| |
| if (!fFeatures.contains(Features.VERSION_IS_SUPPORTED)) { |
| /* Unsupported LAMI version */ |
| return; |
| } |
| |
| /* |
| * Step 3: Check the metadata. This determines if the analysis is |
| * supported at all or not. |
| */ |
| if (checkMetadata()) { |
| fFeatures.add(Features.SUPPORTED); |
| } |
| } |
| |
| /** |
| * Verify that this script returns valid metadata. |
| * |
| * @return True if the command outputs a valid metadata object |
| */ |
| @VisibleForTesting |
| protected boolean checkMetadata() { |
| /* |
| * The initialize() phase of the analysis will be used to check the |
| * script's metadata. Actual runs of the script will use the execute() |
| * method. |
| */ |
| List<String> command = ImmutableList.<@NonNull String> builder() |
| .addAll(fScriptCommand).add(METADATA_FLAG).build(); |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.INFO, RUNNING_METADATA_COMMAND, COMMAND, command); |
| String output = getOutputFromCommand(command); |
| if (output == null || output.isEmpty()) { |
| return false; |
| } |
| |
| /* |
| * |
| * Metadata should look this this: |
| * |
| * { |
| * "version": [1, 5, 2, "dev"], |
| * "title": "I/O latency statistics", |
| * "authors": [ |
| * "Julien Desfossez", |
| * "Antoine Busque" |
| * ], |
| * "description": "Provides statistics about the latency involved in various I/O operations.", |
| * "url": "https://github.com/lttng/lttng-analyses", |
| * "tags": [ |
| * "io", |
| * "stats", |
| * "linux-kernel", |
| * "lttng-analyses" |
| * ], |
| * "table-classes": { |
| * "syscall-latency": { |
| * "title": "System calls latency statistics", |
| * "column-descriptions": [ |
| * {"title": "System call", "type": "syscall"}, |
| * {"title": "Count", "type": "int", "unit": "operations"}, |
| * {"title": "Minimum duration", "type": "duration"}, |
| * {"title": "Average duration", "type": "duration"}, |
| * {"title": "Maximum duration", "type": "duration"}, |
| * {"title": "Standard deviation", "type": "duration"} |
| * ] |
| * }, |
| * "disk-latency": { |
| * "title": "Disk latency statistics", |
| * "column-descriptions": [ |
| * {"title": "Disk name", "type": "disk"}, |
| * {"title": "Count", "type": "int", "unit": "operations"}, |
| * {"title": "Minimum duration", "type": "duration"}, |
| * {"title": "Average duration", "type": "duration"}, |
| * {"title": "Maximum duration", "type": "duration"}, |
| * {"title": "Standard deviation", "type": "duration"} |
| * ] |
| * } |
| * } |
| * } |
| * |
| */ |
| |
| try { |
| JSONObject obj = new JSONObject(output); |
| fAnalysisTitle = obj.getString(LamiStrings.TITLE); |
| |
| JSONObject tableClasses = obj.getJSONObject(LamiStrings.TABLE_CLASSES); |
| @NonNull String[] tableClassNames = checkNotNullContents(JSONObject.getNames(tableClasses)); |
| |
| ImmutableMap.Builder<String, LamiTableClass> tablesBuilder = ImmutableMap.builder(); |
| for (String tableClassName : tableClassNames) { |
| JSONObject tableClass = tableClasses.getJSONObject(tableClassName); |
| |
| final String tableTitle = checkNotNull(tableClass.getString(LamiStrings.TITLE)); |
| @NonNull JSONArray columnDescriptions = checkNotNull(tableClass.getJSONArray(LamiStrings.COLUMN_DESCRIPTIONS)); |
| |
| List<LamiTableEntryAspect> aspects = getAspectsFromColumnDescriptions(columnDescriptions); |
| Collection<LamiChartModel> chartModels = getPredefinedCharts().get(tableClassName); |
| |
| tablesBuilder.put(tableClassName, new LamiTableClass(tableClassName, tableTitle, aspects, chartModels)); |
| } |
| |
| try { |
| fTableClasses = tablesBuilder.build(); |
| } catch (IllegalArgumentException e) { |
| /* |
| * This is thrown if there are duplicate keys in the map |
| * builder. |
| */ |
| throw new JSONException("Duplicate table class entry in " + fAnalysisTitle); //$NON-NLS-1$ |
| } |
| |
| } catch (JSONException e) { |
| /* Error in the parsing of the JSON, script is broken? */ |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.WARNING, ERROR_PARSING_METADATA, e.getMessage()); |
| return false; |
| } |
| return true; |
| } |
| |
| private static List<LamiTableEntryAspect> getAspectsFromColumnDescriptions(JSONArray columnDescriptions) throws JSONException { |
| ImmutableList.Builder<LamiTableEntryAspect> aspectsBuilder = new ImmutableList.Builder<>(); |
| for (int j = 0; j < columnDescriptions.length(); j++) { |
| JSONObject column = columnDescriptions.getJSONObject(j); |
| DataType columnDataType; |
| String columnClass = column.optString(LamiStrings.CLASS, null); |
| |
| if (columnClass == null) { |
| columnDataType = DataType.MIXED; |
| } else { |
| columnDataType = getDataTypeFromString(columnClass); |
| } |
| |
| String columnTitle = column.optString(LamiStrings.TITLE, null); |
| |
| if (columnTitle == null) { |
| columnTitle = String.format("%s #%d", columnDataType.getTitle(), j + 1); //$NON-NLS-1$ |
| } |
| |
| final int colIndex = j; |
| switch (columnDataType) { |
| case TIME_RANGE: |
| /* |
| * We will add 3 aspects, to represent the start, end and |
| * duration of this time range. |
| */ |
| aspectsBuilder.add(new LamiTimeRangeBeginAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiTimeRangeEndAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiTimeRangeDurationAspect(columnTitle, colIndex)); |
| break; |
| |
| case TIMESTAMP: |
| aspectsBuilder.add(new LamiTimestampAspect(columnTitle, colIndex)); |
| break; |
| |
| case PROCESS: |
| aspectsBuilder.add(new LamiProcessNameAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiProcessPIDAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiProcessTIDAspect(columnTitle, colIndex)); |
| break; |
| |
| case IRQ: |
| aspectsBuilder.add(new LamiIRQTypeAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiIRQNameAspect(columnTitle, colIndex)); |
| aspectsBuilder.add(new LamiIRQNumberAspect(columnTitle, colIndex)); |
| break; |
| |
| case DURATION: |
| aspectsBuilder.add(new LamiDurationAspect(columnTitle, colIndex)); |
| break; |
| |
| case MIXED: |
| aspectsBuilder.add(new LamiMixedAspect(columnTitle, colIndex)); |
| break; |
| |
| // $CASES-OMITTED$ |
| default: |
| String units = column.optString(LamiStrings.UNIT, null); |
| |
| if (units == null) { |
| units = columnDataType.getUnits(); |
| } |
| |
| /* We will add only one aspect representing the element */ |
| LamiTableEntryAspect aspect = new LamiGenericAspect(columnTitle, |
| units, colIndex, columnDataType.isContinuous(), false); |
| aspectsBuilder.add(aspect); |
| break; |
| } |
| } |
| /* |
| * SWT quirk : we need an empty column at the end or else the last data |
| * column will clamp to the right edge of the view if it is |
| * right-aligned. |
| */ |
| aspectsBuilder.add(LamiEmptyAspect.INSTANCE); |
| |
| return aspectsBuilder.build(); |
| } |
| |
| private static DataType getDataTypeFromString(String value) throws JSONException { |
| try { |
| return DataType.fromString(value); |
| } catch (IllegalArgumentException e) { |
| throw new JSONException("Unrecognized data type: " + value); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Get the title of this analysis, as read from the script's metadata. |
| * |
| * @return The analysis title. Should not be null after the initialization |
| * completed successfully. |
| */ |
| public @Nullable String getAnalysisTitle() { |
| return fAnalysisTitle; |
| } |
| |
| /** |
| * Get the result table classes defined by this analysis, as read from the |
| * script's metadata. |
| * |
| * @return The analysis' result table classes. Should not be null after the |
| * execution completed successfully. |
| */ |
| public @Nullable Map<String, LamiTableClass> getTableClasses() { |
| return fTableClasses; |
| } |
| |
| /** |
| * Print the full command that will be run when calling {@link #execute}, |
| * with the exception of the 'extraParams' that will be passed to execute(). |
| * |
| * This can be used to display the command in the UI before it is actually |
| * run. |
| * |
| * @param trace |
| * The trace on which to run the analysis |
| * @param range |
| * The time range to specify. Null will not specify a time range, |
| * which means the whole trace will be taken. |
| * @return The command as a single, space-separated string |
| */ |
| public String getFullCommandAsString(ITmfTrace trace, @Nullable TmfTimeRange range) { |
| String tracePath = checkNotNull(trace.getPath()); |
| |
| ImmutableList.Builder<String> builder = getBaseCommand(range); |
| /* |
| * We can add double-quotes around the trace path, which could contain |
| * spaces, so that the resulting command can be easily copy-pasted into |
| * a shell. |
| */ |
| builder.add(DOUBLE_QUOTES + tracePath + DOUBLE_QUOTES); |
| return Objects.requireNonNull(String.join(" ", builder.build())); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Get the base part of the command that will be executed to run this |
| * analysis, supplying the given time range. Base part meaning: |
| * |
| * <pre> |
| * [script executable] [statically-defined parameters] [--begin/--end (if applicable)] |
| * </pre> |
| * |
| * Note that it does not include the path to the trace, that is to be added |
| * separately. |
| * |
| * @param range |
| * The time range that will be passed |
| * @return The elements of the command |
| */ |
| private ImmutableList.Builder<String> getBaseCommand(@Nullable TmfTimeRange range) { |
| long begin = 0; |
| long end = 0; |
| if (range != null) { |
| begin = range.getStartTime().getValue(); |
| end = range.getEndTime().getValue(); |
| if (range.getStartTime().compareTo(range.getEndTime()) > 0) { |
| begin = range.getEndTime().getValue(); |
| end = range.getStartTime().getValue(); |
| } |
| } |
| |
| ImmutableList.Builder<String> builder = ImmutableList.builder(); |
| builder.addAll(fScriptCommand); |
| |
| if (fFeatures.contains(Features.OUTPUT_PROGRESS)) { |
| builder.add(PROGRESS_FLAG); |
| } |
| |
| if (range != null) { |
| builder.add(BEGIN_FLAG).add(String.valueOf(begin)); |
| builder.add(END_FLAG).add(String.valueOf(end)); |
| } |
| return builder; |
| } |
| |
| @Override |
| public List<LamiResultTable> execute(ITmfTrace trace, @Nullable TmfTimeRange timeRange, |
| String extraParamsString, IProgressMonitor monitor) throws CoreException { |
| /* Should have been called already, but in case it was not */ |
| initialize(); |
| |
| final @NonNull String tracePath = checkNotNull(trace.getPath()); |
| final @NonNull String trimmedExtraParamsString = checkNotNull(extraParamsString.trim()); |
| final List<String> extraParams = ShellUtils.commandStringToArgs(trimmedExtraParamsString); |
| |
| ImmutableList.Builder<String> builder = getBaseCommand(timeRange); |
| |
| builder.addAll(extraParams); |
| builder.add(tracePath); |
| List<String> command = builder.build(); |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.INFO, RUNNING_EXECUTE_COMMAND, COMMAND, command); |
| String output = getResultsFromCommand(command, monitor); |
| |
| if (output.isEmpty()) { |
| IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults); |
| throw new CoreException(status); |
| } |
| |
| /* |
| * { |
| * "results": [ |
| * { |
| * "time-range": { |
| * "type": "time-range", |
| * "begin": 1444334398154194201, |
| * "end": 1444334425194487548 |
| * }, |
| * "class": "syscall-latency", |
| * "data": [ |
| * [ |
| * {"type": "syscall", "name": "open"}, |
| * 45, |
| * {"type": "duration", "value": 5562}, |
| * {"type": "duration", "value": 13835}, |
| * {"type": "duration", "value": 77683}, |
| * {"type": "duration", "value": 15263} |
| * ], |
| * [ |
| * {"type": "syscall", "name": "read"}, |
| * 109, |
| * {"type": "duration", "value": 316}, |
| * {"type": "duration", "value": 5774}, |
| * {"type": "duration", "value": 62569}, |
| * {"type": "duration", "value": 9277} |
| * ] |
| * ] |
| * }, |
| * { |
| * "time-range": { |
| * "type": "time-range", |
| * "begin": 1444334425194487549, |
| * "end": 1444334425254887190 |
| * }, |
| * "class": "syscall-latency", |
| * "data": [ |
| * [ |
| * {"type": "syscall", "name": "open"}, |
| * 45, |
| * {"type": "duration", "value": 1578}, |
| * {"type": "duration", "value": 16648}, |
| * {"type": "duration", "value": 15444}, |
| * {"type": "duration", "value": 68540} |
| * ], |
| * [ |
| * {"type": "syscall", "name": "read"}, |
| * 109, |
| * {"type": "duration", "value": 78}, |
| * {"type": "duration", "value": 1948}, |
| * {"type": "duration", "value": 11184}, |
| * {"type": "duration", "value": 94670} |
| * ] |
| * ] |
| * } |
| * ] |
| * } |
| * |
| */ |
| |
| ImmutableList.Builder<LamiResultTable> resultsBuilder = new ImmutableList.Builder<>(); |
| |
| try { |
| JSONObject obj = new JSONObject(output); |
| JSONArray results = obj.getJSONArray(LamiStrings.RESULTS); |
| |
| if (results.length() == 0) { |
| /* |
| * No results were reported. This may be normal, but warn the |
| * user why a report won't be created. |
| */ |
| IStatus status = new Status(IStatus.INFO, Activator.instance().getPluginId(), Messages.LamiAnalysis_NoResults); |
| throw new CoreException(status); |
| } |
| |
| for (int i = 0; i < results.length(); i++) { |
| JSONObject result = results.getJSONObject(i); |
| |
| /* Parse the time-range */ |
| JSONObject trObject = checkNotNull(result.getJSONObject(LamiStrings.TIME_RANGE)); |
| LamiData trData = LamiData.createFromObject(trObject); |
| if (!(trData instanceof LamiTimeRange)) { |
| throw new JSONException("Time range did not have expected class type."); //$NON-NLS-1$ |
| } |
| LamiTimeRange tr = (LamiTimeRange) trData; |
| |
| /* Parse the table's class */ |
| LamiTableClass tableClass; |
| JSONObject tableClassObject = result.optJSONObject(LamiStrings.CLASS); |
| if (tableClassObject == null) { |
| /* |
| * "class" is just a standard string, indicating we use a |
| * metadata-defined table class as-is |
| */ |
| @NonNull String tableClassName = checkNotNull(result.getString(LamiStrings.CLASS)); |
| tableClass = getTableClassFromName(tableClassName); |
| |
| // FIXME Rest will become more generic eventually in the LAMI format. |
| } else if (tableClassObject.has(LamiStrings.INHERIT)) { |
| /* |
| * Dynamic title: We reuse an existing table class but |
| * override the title. |
| */ |
| String baseTableName = checkNotNull(tableClassObject.getString(LamiStrings.INHERIT)); |
| LamiTableClass baseTableClass = getTableClassFromName(baseTableName); |
| String newTitle = checkNotNull(tableClassObject.getString(LamiStrings.TITLE)); |
| |
| tableClass = new LamiTableClass(baseTableClass, newTitle); |
| } else { |
| /* |
| * Dynamic column descriptions: we implement a new table |
| * class entirely. |
| */ |
| String title = checkNotNull(tableClassObject.getString(LamiStrings.TITLE)); |
| JSONArray columnDescriptions = checkNotNull(tableClassObject.getJSONArray(LamiStrings.COLUMN_DESCRIPTIONS)); |
| List<LamiTableEntryAspect> aspects = getAspectsFromColumnDescriptions(columnDescriptions); |
| |
| tableClass = new LamiTableClass(nullToEmptyString(Messages.LamiAnalysis_DefaultDynamicTableName), title, aspects, Collections.emptySet()); |
| } |
| |
| /* Parse the "data", which is the array of rows */ |
| JSONArray data = result.getJSONArray(LamiStrings.DATA); |
| ImmutableList.Builder<LamiTableEntry> dataBuilder = new ImmutableList.Builder<>(); |
| |
| for (int j = 0; j < data.length(); j++) { |
| /* A row is an array of cells */ |
| JSONArray row = data.getJSONArray(j); |
| ImmutableList.Builder<LamiData> rowBuilder = ImmutableList.builder(); |
| |
| for (int k = 0; k < row.length(); k++) { |
| Object cellObject = checkNotNull(row.get(k)); |
| LamiData cellValue = LamiData.createFromObject(cellObject); |
| rowBuilder.add(cellValue); |
| |
| } |
| dataBuilder.add(new LamiTableEntry(rowBuilder.build())); |
| } |
| resultsBuilder.add(new LamiResultTable(tr, tableClass, dataBuilder.build())); |
| } |
| |
| } catch (JSONException e) { |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.WARNING, ERROR_PARSING_EXECUTION_OUTPUT, e.getMessage()); |
| IStatus status = new Status(IStatus.ERROR, Activator.instance().getPluginId(), e.getMessage(), e); |
| throw new CoreException(status); |
| } |
| |
| return resultsBuilder.build(); |
| } |
| |
| private LamiTableClass getTableClassFromName(String tableClassName) throws JSONException { |
| Map<String, LamiTableClass> map = checkNotNull(fTableClasses); |
| LamiTableClass tableClass = map.get(tableClassName); |
| if (tableClass == null) { |
| throw new JSONException("Table class " + tableClassName + //$NON-NLS-1$ |
| " was not declared in the metadata"); //$NON-NLS-1$ |
| } |
| return tableClass; |
| } |
| |
| /** |
| * Get the output of an external command, used for getting the metadata. |
| * Cannot be cancelled, and will not report errors, simply returns null if |
| * the process ended abnormally. |
| * |
| * @param command |
| * The parameters of the command, passed to |
| * {@link ProcessBuilder} |
| * @return The command output as a string |
| */ |
| @VisibleForTesting |
| protected @Nullable String getOutputFromCommand(List<String> command) { |
| TraceCompassLogUtils.traceInstant(LOGGER, Level.FINE, LOG_RUNNING_MESSAGE, COMMAND, command); |
| List<String> lines = ProcessUtils.getOutputFromCommand(command); |
| if (lines == null) { |
| return null; |
| } |
| return String.join("", lines); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Get the results of invoking the specified command. |
| * |
| * The result should start with '{"results":...', as specified by the LAMI |
| * JSON protocol. The JSON itself may be split over multiple lines. |
| * |
| * @param command |
| * The command to run (program and its arguments) |
| * @param monitor |
| * The progress monitor |
| * @return The analysis results |
| * @throws CoreException |
| * If the command ended abnormally, and normal results were not |
| * returned |
| */ |
| @VisibleForTesting |
| protected String getResultsFromCommand(List<String> command, IProgressMonitor monitor) |
| throws CoreException { |
| List<String> lines = ProcessUtils.getOutputFromCommandCancellable(command, monitor, nullToEmptyString(Messages.LamiAnalysis_MainTaskName), OUTPUT_READER); |
| return checkNotNull(String.join("", lines)); //$NON-NLS-1$ |
| } |
| |
| private static final OutputReaderFunction OUTPUT_READER = (reader, monitor) -> { |
| double workedSoFar = 0.0; |
| |
| String line = reader.readLine(); |
| while (line != null && !line.matches("\\s*\\{.*")) { //$NON-NLS-1$ |
| /* |
| * This is a line indicating progress, it has the form: |
| * |
| * 0.123 3000 of 5000 events processed |
| * |
| * The first part indicates the estimated fraction (out of 1.0) of |
| * work done. The second part is status text. |
| */ |
| |
| // Trim the line first to make sure the first character is |
| // significant |
| line = line.trim(); |
| |
| // Split at the first space |
| String[] elems = line.split(" ", 2); //$NON-NLS-1$ |
| |
| if (elems[0].matches("\\d.*")) { //$NON-NLS-1$ |
| // It looks like we have a progress indication |
| try { |
| // Try parsing the number |
| double cumulativeWork = Double.parseDouble(elems[0]) * 1000; |
| double workedThisLoop = cumulativeWork - workedSoFar; |
| |
| // We're going backwards? Do not update the |
| // monitor's value |
| if (workedThisLoop > 0) { |
| monitor.internalWorked(workedThisLoop); |
| workedSoFar = cumulativeWork; |
| } |
| |
| // There is a message: update the monitor's task name |
| if (elems.length >= 2) { |
| monitor.setTaskName(elems[1].trim()); |
| } |
| } catch (NumberFormatException e) { |
| // Continue reading progress lines anyway |
| } |
| } |
| |
| line = reader.readLine(); |
| } |
| |
| List<String> results = new ArrayList<>(); |
| while (line != null) { |
| /* |
| * We have seen the first line containing a '{', this is our JSON |
| * output! |
| */ |
| results.add(line); |
| line = reader.readLine(); |
| } |
| return results; |
| }; |
| |
| |
| @Override |
| public @NonNull String getName() { |
| return fName; |
| } |
| |
| @Override |
| public boolean isUserDefined() { |
| return fIsUserDefined; |
| } |
| |
| } |