1 /*******************************************************************************
2 * Copyright (c) 2015, 2016 EfficiOS Inc., Alexandre Montplaisir
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *******************************************************************************/
10 package org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.module
;
12 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNull
;
13 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.checkNotNullContents
;
14 import static org
.eclipse
.tracecompass
.common
.core
.NonNullUtils
.nullToEmptyString
;
17 import java
.nio
.file
.Files
;
18 import java
.nio
.file
.Paths
;
19 import java
.util
.ArrayList
;
20 import java
.util
.Collection
;
21 import java
.util
.Collections
;
22 import java
.util
.EnumSet
;
23 import java
.util
.List
;
25 import java
.util
.WeakHashMap
;
26 import java
.util
.function
.Predicate
;
27 import java
.util
.logging
.Logger
;
28 import java
.util
.regex
.Pattern
;
29 import java
.util
.stream
.Collectors
;
30 import java
.util
.stream
.Stream
;
32 import org
.eclipse
.core
.runtime
.CoreException
;
33 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
34 import org
.eclipse
.core
.runtime
.IStatus
;
35 import org
.eclipse
.core
.runtime
.Status
;
36 import org
.eclipse
.jdt
.annotation
.NonNull
;
37 import org
.eclipse
.jdt
.annotation
.Nullable
;
38 import org
.eclipse
.tracecompass
.common
.core
.log
.TraceCompassLog
;
39 import org
.eclipse
.tracecompass
.common
.core
.process
.ProcessUtils
;
40 import org
.eclipse
.tracecompass
.common
.core
.process
.ProcessUtils
.OutputReaderFunction
;
41 import org
.eclipse
.tracecompass
.internal
.analysis
.lami
.core
.Activator
;
42 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.LamiStrings
;
43 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.ShellUtils
;
44 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiDurationAspect
;
45 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiEmptyAspect
;
46 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiGenericAspect
;
47 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQNameAspect
;
48 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQNumberAspect
;
49 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiIRQTypeAspect
;
50 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiMixedAspect
;
51 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessNameAspect
;
52 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessPIDAspect
;
53 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiProcessTIDAspect
;
54 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTableEntryAspect
;
55 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeBeginAspect
;
56 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeDurationAspect
;
57 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimeRangeEndAspect
;
58 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.aspect
.LamiTimestampAspect
;
59 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiData
;
60 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiData
.DataType
;
61 import org
.eclipse
.tracecompass
.internal
.provisional
.analysis
.lami
.core
.types
.LamiTimeRange
;
62 import org
.eclipse
.tracecompass
.tmf
.core
.analysis
.ondemand
.IOnDemandAnalysis
;
63 import org
.eclipse
.tracecompass
.tmf
.core
.timestamp
.TmfTimeRange
;
64 import org
.eclipse
.tracecompass
.tmf
.core
.trace
.ITmfTrace
;
65 import org
.json
.JSONArray
;
66 import org
.json
.JSONException
;
67 import org
.json
.JSONObject
;
69 import com
.google
.common
.annotations
.VisibleForTesting
;
70 import com
.google
.common
.collect
.ImmutableList
;
71 import com
.google
.common
.collect
.ImmutableMap
;
72 import com
.google
.common
.collect
.ImmutableMultimap
;
73 import com
.google
.common
.collect
.Multimap
;
76 * Base class for analysis modules that call external scripts implementing the
79 * @author Alexandre Montplaisir
80 * @author Philippe Proulx
82 public class LamiAnalysis
implements IOnDemandAnalysis
{
84 private static final Logger LOGGER
= TraceCompassLog
.getLogger(LamiAnalysis
.class);
85 private static final String DOUBLE_QUOTES
= "\""; //$NON-NLS-1$
87 /* Flags passed to the analysis scripts */
88 private static final String MI_VERSION_FLAG
= "--mi-version"; //$NON-NLS-1$
89 private static final String TEST_COMPATIBILITY_FLAG
= "--test-compatibility"; //$NON-NLS-1$
90 private static final String METADATA_FLAG
= "--metadata"; //$NON-NLS-1$
91 private static final String PROGRESS_FLAG
= "--output-progress"; //$NON-NLS-1$
92 private static final String BEGIN_FLAG
= "--begin"; //$NON-NLS-1$
93 private static final String END_FLAG
= "--end"; //$NON-NLS-1$
96 private static final String LOG_RUNNING_MESSAGE
= "[LamiAnalysis:RunningCommand] "; //$NON-NLS-1$
97 private static final String LOG_NO_MI_VERSION_FMT
= "[LamiAnalysis:InvalidMIVersionReport] Command \"%s\" reports no specific or invalid MI version"; //$NON-NLS-1$
99 /* Tokens of the complete command */
100 private final List
<String
> fScriptCommand
;
102 /* 0 means pre-1.0 LAMI protocol */
103 private int fMiVersion
= 0;
105 /* Custom name given to this analysis. */
106 private final String fName
;
109 * True if this analysis is defined by a user, that is, not
112 private final boolean fIsUserDefined
;
115 * The predicate to use to determine if a given trace applies
116 * to this analysis, that is, the given trace's type is compatible
117 * with this analysis a priori. The analysis might not be able to
118 * execute for a given trace, but this is returned by canExecute().
120 private final Predicate
<ITmfTrace
> fAppliesTo
;
122 /* Data defined by the analysis's metadata */
123 private @Nullable String fAnalysisTitle
;
124 private @Nullable Map
<String
, LamiTableClass
> fTableClasses
;
126 /* Cache: true if the initialization process took place already. */
127 private boolean fInitialized
= false;
130 * Cache: assigns a boolean to a trace which indicates if this analysis can
131 * be executed on the given trace. Presence in the map indicates the
132 * compatibility test has already run, so "false" is different than "absent"
135 * Uses weak keys, so we do not hold references to trace objects and prevent
136 * them from being disposed.
138 private final Map
<ITmfTrace
, Boolean
> fTraceCompatibilityCache
= new WeakHashMap
<>();
141 * Available features.
143 private enum Features
{
144 /** The MI version of the analysis is supported. */
145 VERSION_IS_SUPPORTED
,
147 /** The analysis is supported at all. */
150 /** The analysis supports a progress indication output. */
153 /** The analysis supports testing a given trace for compatibility. */
157 /* Available features depending on the MI version */
158 private final EnumSet
<Features
> fFeatures
= checkNotNull(EnumSet
.noneOf(Features
.class));
161 * Constructor. To be called by implementing classes.
164 * Name of this analysis
165 * @param isUserDefined
166 * {@code true} if this is a user-defined analysis
168 * Predicate to use to check whether or not this analysis applies
171 * Analysis arguments, including the executable name (first
174 public LamiAnalysis(String name
, boolean isUserDefined
, Predicate
<ITmfTrace
> appliesTo
,
176 fScriptCommand
= ImmutableList
.copyOf(args
);
178 fIsUserDefined
= isUserDefined
;
179 fAppliesTo
= appliesTo
;
183 * Map of pre-defined charts, for every table class names.
185 * If a table class is not in this map then it means that table has no
188 * @return The chart models, per table class names
190 protected Multimap
<String
, LamiChartModel
> getPredefinedCharts() {
191 return ImmutableMultimap
.of();
195 public final boolean appliesTo(ITmfTrace trace
) {
196 return fAppliesTo
.test(trace
);
199 private boolean testCompatibility(ITmfTrace trace
) {
200 final @NonNull String tracePath
= checkNotNull(trace
.getPath());
202 final List
<String
> commandLine
= ImmutableList
.<@NonNull String
> builder()
203 .addAll(fScriptCommand
)
204 .add(TEST_COMPATIBILITY_FLAG
)
207 final boolean isCompatible
= (getOutputFromCommand(commandLine
) != null);
209 /* Add this result to the compatibility cache. */
210 fTraceCompatibilityCache
.put(trace
, isCompatible
);
215 private boolean isSupported() {
218 return fFeatures
.contains(Features
.SUPPORTED
);
222 public boolean canExecute(ITmfTrace trace
) {
223 /* Make sure this analysis is supported at all. */
224 if (!isSupported()) {
228 if (!fFeatures
.contains(Features
.TEST_COMPATIBILITY
)) {
230 * No support for dynamic compatibility testing: suppose this
231 * analysis can run on any trace.
236 /* Check if this trace is already registered in the cache. */
237 if (fTraceCompatibilityCache
.getOrDefault(trace
, false)) {
242 * Test compatibility since it's supported.
244 return testCompatibility(trace
);
247 private void setFeatures() {
248 if (fMiVersion
== 0) {
249 // Pre-1.0 LAMI protocol: supported for backward compatibility
250 fFeatures
.add(Features
.VERSION_IS_SUPPORTED
);
254 if (fMiVersion
>= 100 && fMiVersion
< 200) {
256 fFeatures
.add(Features
.VERSION_IS_SUPPORTED
);
257 fFeatures
.add(Features
.OUTPUT_PROGRESS
);
258 fFeatures
.add(Features
.TEST_COMPATIBILITY
);
262 private void readVersion() {
263 final String command
= fScriptCommand
.get(0);
264 final List
<String
> commandLine
= ImmutableList
.<@NonNull String
> builder()
265 .add(command
).add(MI_VERSION_FLAG
).build();
266 final String output
= getOutputFromCommand(commandLine
);
268 if (output
== null) {
269 LOGGER
.info(() -> String
.format(LOG_NO_MI_VERSION_FMT
, command
));
273 final String versionString
= output
.trim();
275 if (!versionString
.matches("\\d{1,3}\\.\\d{1,3}")) { //$NON-NLS-1$
276 LOGGER
.info(() -> String
.format(LOG_NO_MI_VERSION_FMT
, command
));
280 LOGGER
.info(() -> String
.format("[LamiAnalysis:MIVersionReport] Command \"%s\" reports MI version %s", //$NON-NLS-1$
281 command
, versionString
));
283 final String
[] parts
= versionString
.split("\\."); //$NON-NLS-1$
284 final int major
= Integer
.valueOf(parts
[0]);
285 final int minor
= Integer
.valueOf(parts
[1]);
287 fMiVersion
= major
* 100 + minor
;
290 private static boolean executableExists(String name
) {
291 if (name
.contains(File
.separator
)) {
292 /* This seems like a path, not just an executable name */
293 return Files
.isExecutable(Paths
.get(name
));
296 /* Check if this name is found in the PATH environment variable */
297 final String pathEnv
= System
.getenv("PATH"); //$NON-NLS-1$
298 final String
[] exeDirs
= pathEnv
.split(checkNotNull(Pattern
.quote(File
.pathSeparator
)));
300 return Stream
.of(exeDirs
)
302 .anyMatch(path
-> Files
.isExecutable(path
.resolve(name
)));
306 * Perform initialization of the LAMI script. This means verifying that it
307 * is actually present on disk, and that it returns correct --metadata.
310 protected synchronized void initialize() {
312 /* Already initialized */
318 /* Step 1: Check if the script's expected executable is on the PATH. */
319 final String executable
= fScriptCommand
.get(0);
320 final boolean executableExists
= executableExists(executable
);
322 if (!executableExists
) {
323 /* Script is not found */
328 * Step 2: Read the version, which also gives us an indication as
329 * to whether or not this executable supports LAMI 1.0.
334 * Set the available features according to the MI version.
338 if (!fFeatures
.contains(Features
.VERSION_IS_SUPPORTED
)) {
339 /* Unsupported LAMI version */
344 * Step 3: Check the metadata. This determines if the analysis is
345 * supported at all or not.
347 if (checkMetadata()) {
348 fFeatures
.add(Features
.SUPPORTED
);
353 * Verify that this script returns valid metadata.
355 * @return True if the command outputs a valid metadata object
358 protected boolean checkMetadata() {
360 * The initialize() phase of the analysis will be used to check the
361 * script's metadata. Actual runs of the script will use the execute()
364 List
<String
> command
= ImmutableList
.<@NonNull String
> builder()
365 .addAll(fScriptCommand
).add(METADATA_FLAG
).build();
366 LOGGER
.info(() -> "[LamiAnalysis:RunningMetadataCommand] " + command
.toString()); //$NON-NLS-1$
367 String output
= getOutputFromCommand(command
);
368 if (output
== null || output
.isEmpty()) {
374 * Metadata should look this this:
377 * "version": [1, 5, 2, "dev"],
378 * "title": "I/O latency statistics",
380 * "Julien Desfossez",
383 * "description": "Provides statistics about the latency involved in various I/O operations.",
384 * "url": "https://github.com/lttng/lttng-analyses",
392 * "syscall-latency": {
393 * "title": "System calls latency statistics",
394 * "column-descriptions": [
395 * {"title": "System call", "type": "syscall"},
396 * {"title": "Count", "type": "int", "unit": "operations"},
397 * {"title": "Minimum duration", "type": "duration"},
398 * {"title": "Average duration", "type": "duration"},
399 * {"title": "Maximum duration", "type": "duration"},
400 * {"title": "Standard deviation", "type": "duration"}
404 * "title": "Disk latency statistics",
405 * "column-descriptions": [
406 * {"title": "Disk name", "type": "disk"},
407 * {"title": "Count", "type": "int", "unit": "operations"},
408 * {"title": "Minimum duration", "type": "duration"},
409 * {"title": "Average duration", "type": "duration"},
410 * {"title": "Maximum duration", "type": "duration"},
411 * {"title": "Standard deviation", "type": "duration"}
420 JSONObject obj
= new JSONObject(output
);
421 fAnalysisTitle
= obj
.getString(LamiStrings
.TITLE
);
423 JSONObject tableClasses
= obj
.getJSONObject(LamiStrings
.TABLE_CLASSES
);
424 @NonNull String
[] tableClassNames
= checkNotNullContents(JSONObject
.getNames(tableClasses
));
426 ImmutableMap
.Builder
<String
, LamiTableClass
> tablesBuilder
= ImmutableMap
.builder();
427 for (String tableClassName
: tableClassNames
) {
428 JSONObject tableClass
= tableClasses
.getJSONObject(tableClassName
);
430 final String tableTitle
= checkNotNull(tableClass
.getString(LamiStrings
.TITLE
));
431 @NonNull JSONArray columnDescriptions
= checkNotNull(tableClass
.getJSONArray(LamiStrings
.COLUMN_DESCRIPTIONS
));
433 List
<LamiTableEntryAspect
> aspects
= getAspectsFromColumnDescriptions(columnDescriptions
);
434 Collection
<LamiChartModel
> chartModels
= getPredefinedCharts().get(tableClassName
);
436 tablesBuilder
.put(tableClassName
, new LamiTableClass(tableClassName
, tableTitle
, aspects
, chartModels
));
440 fTableClasses
= tablesBuilder
.build();
441 } catch (IllegalArgumentException e
) {
443 * This is thrown if there are duplicate keys in the map
446 throw new JSONException("Duplicate table class entry in " + fAnalysisTitle
); //$NON-NLS-1$
449 } catch (JSONException e
) {
450 /* Error in the parsing of the JSON, script is broken? */
451 LOGGER
.severe(() -> "[LamiAnalysis:ErrorParsingMetadata] msg=" + e
.getMessage()); //$NON-NLS-1$
457 private static List
<LamiTableEntryAspect
> getAspectsFromColumnDescriptions(JSONArray columnDescriptions
) throws JSONException
{
458 ImmutableList
.Builder
<LamiTableEntryAspect
> aspectsBuilder
= new ImmutableList
.Builder
<>();
459 for (int j
= 0; j
< columnDescriptions
.length(); j
++) {
460 JSONObject column
= columnDescriptions
.getJSONObject(j
);
461 DataType columnDataType
;
462 String columnClass
= column
.optString(LamiStrings
.CLASS
, null);
464 if (columnClass
== null) {
465 columnDataType
= DataType
.MIXED
;
467 columnDataType
= getDataTypeFromString(columnClass
);
470 String columnTitle
= column
.optString(LamiStrings
.TITLE
, null);
472 if (columnTitle
== null) {
473 columnTitle
= String
.format("%s #%d", columnDataType
.getTitle(), j
+ 1); //$NON-NLS-1$
476 final int colIndex
= j
;
477 switch (columnDataType
) {
480 * We will add 3 aspects, to represent the start, end and
481 * duration of this time range.
483 aspectsBuilder
.add(new LamiTimeRangeBeginAspect(columnTitle
, colIndex
));
484 aspectsBuilder
.add(new LamiTimeRangeEndAspect(columnTitle
, colIndex
));
485 aspectsBuilder
.add(new LamiTimeRangeDurationAspect(columnTitle
, colIndex
));
489 aspectsBuilder
.add(new LamiTimestampAspect(columnTitle
, colIndex
));
493 aspectsBuilder
.add(new LamiProcessNameAspect(columnTitle
, colIndex
));
494 aspectsBuilder
.add(new LamiProcessPIDAspect(columnTitle
, colIndex
));
495 aspectsBuilder
.add(new LamiProcessTIDAspect(columnTitle
, colIndex
));
499 aspectsBuilder
.add(new LamiIRQTypeAspect(columnTitle
, colIndex
));
500 aspectsBuilder
.add(new LamiIRQNameAspect(columnTitle
, colIndex
));
501 aspectsBuilder
.add(new LamiIRQNumberAspect(columnTitle
, colIndex
));
505 aspectsBuilder
.add(new LamiDurationAspect(columnTitle
, colIndex
));
509 aspectsBuilder
.add(new LamiMixedAspect(columnTitle
, colIndex
));
514 String units
= column
.optString(LamiStrings
.UNIT
, null);
517 units
= columnDataType
.getUnits();
520 /* We will add only one aspect representing the element */
521 LamiTableEntryAspect aspect
= new LamiGenericAspect(columnTitle
,
522 units
, colIndex
, columnDataType
.isContinuous(), false);
523 aspectsBuilder
.add(aspect
);
528 * SWT quirk : we need an empty column at the end or else the last data
529 * column will clamp to the right edge of the view if it is
532 aspectsBuilder
.add(LamiEmptyAspect
.INSTANCE
);
534 return aspectsBuilder
.build();
537 private static DataType
getDataTypeFromString(String value
) throws JSONException
{
539 return DataType
.fromString(value
);
540 } catch (IllegalArgumentException e
) {
541 throw new JSONException("Unrecognized data type: " + value
); //$NON-NLS-1$
546 * Get the title of this analysis, as read from the script's metadata.
548 * @return The analysis title. Should not be null after the initialization
549 * completed successfully.
551 public @Nullable String
getAnalysisTitle() {
552 return fAnalysisTitle
;
556 * Get the result table classes defined by this analysis, as read from the
559 * @return The analysis' result table classes. Should not be null after the
560 * execution completed successfully.
562 public @Nullable Map
<String
, LamiTableClass
> getTableClasses() {
563 return fTableClasses
;
567 * Print the full command that will be run when calling {@link #execute},
568 * with the exception of the 'extraParams' that will be passed to execute().
570 * This can be used to display the command in the UI before it is actually
574 * The trace on which to run the analysis
576 * The time range to specify. Null will not specify a time range,
577 * which means the whole trace will be taken.
578 * @return The command as a single, space-separated string
580 public String
getFullCommandAsString(ITmfTrace trace
, @Nullable TmfTimeRange range
) {
581 String tracePath
= checkNotNull(trace
.getPath());
583 ImmutableList
.Builder
<String
> builder
= getBaseCommand(range
);
585 * We can add double-quotes around the trace path, which could contain
586 * spaces, so that the resulting command can be easily copy-pasted into
589 builder
.add(DOUBLE_QUOTES
+ tracePath
+ DOUBLE_QUOTES
);
590 List
<String
> list
= builder
.build();
591 String ret
= list
.stream().collect(Collectors
.joining(" ")); //$NON-NLS-1$
592 return checkNotNull(ret
);
596 * Get the base part of the command that will be executed to run this
597 * analysis, supplying the given time range. Base part meaning:
600 * [script executable] [statically-defined parameters] [--begin/--end (if applicable)]
603 * Note that it does not include the path to the trace, that is to be added
607 * The time range that will be passed
608 * @return The elements of the command
610 private ImmutableList
.Builder
<String
> getBaseCommand(@Nullable TmfTimeRange range
) {
614 begin
= range
.getStartTime().getValue();
615 end
= range
.getEndTime().getValue();
616 if (range
.getStartTime().compareTo(range
.getEndTime()) > 0) {
617 begin
= range
.getEndTime().getValue();
618 end
= range
.getStartTime().getValue();
622 ImmutableList
.Builder
<String
> builder
= ImmutableList
.builder();
623 builder
.addAll(fScriptCommand
);
625 if (fFeatures
.contains(Features
.OUTPUT_PROGRESS
)) {
626 builder
.add(PROGRESS_FLAG
);
630 builder
.add(BEGIN_FLAG
).add(String
.valueOf(begin
));
631 builder
.add(END_FLAG
).add(String
.valueOf(end
));
637 public List
<LamiResultTable
> execute(ITmfTrace trace
, @Nullable TmfTimeRange timeRange
,
638 String extraParamsString
, IProgressMonitor monitor
) throws CoreException
{
639 /* Should have been called already, but in case it was not */
642 final @NonNull String tracePath
= checkNotNull(trace
.getPath());
643 final @NonNull String trimmedExtraParamsString
= checkNotNull(extraParamsString
.trim());
644 final List
<String
> extraParams
= ShellUtils
.commandStringToArgs(trimmedExtraParamsString
);
646 ImmutableList
.Builder
<String
> builder
= getBaseCommand(timeRange
);
648 builder
.addAll(extraParams
);
649 builder
.add(tracePath
);
650 List
<String
> command
= builder
.build();
651 LOGGER
.info(() -> "[LamiAnalysis:RunningExecuteCommand] " + command
.toString()); //$NON-NLS-1$
652 String output
= getResultsFromCommand(command
, monitor
);
654 if (output
.isEmpty()) {
655 IStatus status
= new Status(IStatus
.INFO
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_NoResults
);
656 throw new CoreException(status
);
664 * "type": "time-range",
665 * "begin": 1444334398154194201,
666 * "end": 1444334425194487548
668 * "class": "syscall-latency",
671 * {"type": "syscall", "name": "open"},
673 * {"type": "duration", "value": 5562},
674 * {"type": "duration", "value": 13835},
675 * {"type": "duration", "value": 77683},
676 * {"type": "duration", "value": 15263}
679 * {"type": "syscall", "name": "read"},
681 * {"type": "duration", "value": 316},
682 * {"type": "duration", "value": 5774},
683 * {"type": "duration", "value": 62569},
684 * {"type": "duration", "value": 9277}
690 * "type": "time-range",
691 * "begin": 1444334425194487549,
692 * "end": 1444334425254887190
694 * "class": "syscall-latency",
697 * {"type": "syscall", "name": "open"},
699 * {"type": "duration", "value": 1578},
700 * {"type": "duration", "value": 16648},
701 * {"type": "duration", "value": 15444},
702 * {"type": "duration", "value": 68540}
705 * {"type": "syscall", "name": "read"},
707 * {"type": "duration", "value": 78},
708 * {"type": "duration", "value": 1948},
709 * {"type": "duration", "value": 11184},
710 * {"type": "duration", "value": 94670}
719 ImmutableList
.Builder
<LamiResultTable
> resultsBuilder
= new ImmutableList
.Builder
<>();
722 JSONObject obj
= new JSONObject(output
);
723 JSONArray results
= obj
.getJSONArray(LamiStrings
.RESULTS
);
725 if (results
.length() == 0) {
727 * No results were reported. This may be normal, but warn the
728 * user why a report won't be created.
730 IStatus status
= new Status(IStatus
.INFO
, Activator
.instance().getPluginId(), Messages
.LamiAnalysis_NoResults
);
731 throw new CoreException(status
);
734 for (int i
= 0; i
< results
.length(); i
++) {
735 JSONObject result
= results
.getJSONObject(i
);
737 /* Parse the time-range */
738 JSONObject trObject
= checkNotNull(result
.getJSONObject(LamiStrings
.TIME_RANGE
));
739 LamiData trData
= LamiData
.createFromObject(trObject
);
740 if (!(trData
instanceof LamiTimeRange
)) {
741 throw new JSONException("Time range did not have expected class type."); //$NON-NLS-1$
743 LamiTimeRange tr
= (LamiTimeRange
) trData
;
745 /* Parse the table's class */
746 LamiTableClass tableClass
;
747 JSONObject tableClassObject
= result
.optJSONObject(LamiStrings
.CLASS
);
748 if (tableClassObject
== null) {
750 * "class" is just a standard string, indicating we use a
751 * metadata-defined table class as-is
753 @NonNull String tableClassName
= checkNotNull(result
.getString(LamiStrings
.CLASS
));
754 tableClass
= getTableClassFromName(tableClassName
);
756 // FIXME Rest will become more generic eventually in the LAMI format.
757 } else if (tableClassObject
.has(LamiStrings
.INHERIT
)) {
759 * Dynamic title: We reuse an existing table class but
760 * override the title.
762 String baseTableName
= checkNotNull(tableClassObject
.getString(LamiStrings
.INHERIT
));
763 LamiTableClass baseTableClass
= getTableClassFromName(baseTableName
);
764 String newTitle
= checkNotNull(tableClassObject
.getString(LamiStrings
.TITLE
));
766 tableClass
= new LamiTableClass(baseTableClass
, newTitle
);
769 * Dynamic column descriptions: we implement a new table
772 String title
= checkNotNull(tableClassObject
.getString(LamiStrings
.TITLE
));
773 JSONArray columnDescriptions
= checkNotNull(tableClassObject
.getJSONArray(LamiStrings
.COLUMN_DESCRIPTIONS
));
774 List
<LamiTableEntryAspect
> aspects
= getAspectsFromColumnDescriptions(columnDescriptions
);
776 tableClass
= new LamiTableClass(nullToEmptyString(Messages
.LamiAnalysis_DefaultDynamicTableName
), title
, aspects
, Collections
.EMPTY_SET
);
779 /* Parse the "data", which is the array of rows */
780 JSONArray data
= result
.getJSONArray(LamiStrings
.DATA
);
781 ImmutableList
.Builder
<LamiTableEntry
> dataBuilder
= new ImmutableList
.Builder
<>();
783 for (int j
= 0; j
< data
.length(); j
++) {
784 /* A row is an array of cells */
785 JSONArray row
= data
.getJSONArray(j
);
786 ImmutableList
.Builder
<LamiData
> rowBuilder
= ImmutableList
.builder();
788 for (int k
= 0; k
< row
.length(); k
++) {
789 Object cellObject
= checkNotNull(row
.get(k
));
790 LamiData cellValue
= LamiData
.createFromObject(cellObject
);
791 rowBuilder
.add(cellValue
);
794 dataBuilder
.add(new LamiTableEntry(rowBuilder
.build()));
796 resultsBuilder
.add(new LamiResultTable(tr
, tableClass
, dataBuilder
.build()));
799 } catch (JSONException e
) {
800 LOGGER
.severe(() -> "[LamiAnalysis:ErrorParsingExecutionOutput] msg=" + e
.getMessage()); //$NON-NLS-1$
801 IStatus status
= new Status(IStatus
.ERROR
, Activator
.instance().getPluginId(), e
.getMessage(), e
);
802 throw new CoreException(status
);
805 return resultsBuilder
.build();
808 private LamiTableClass
getTableClassFromName(String tableClassName
) throws JSONException
{
809 Map
<String
, LamiTableClass
> map
= checkNotNull(fTableClasses
);
810 LamiTableClass tableClass
= map
.get(tableClassName
);
811 if (tableClass
== null) {
812 throw new JSONException("Table class " + tableClassName
+ //$NON-NLS-1$
813 " was not declared in the metadata"); //$NON-NLS-1$
819 * Get the output of an external command, used for getting the metadata.
820 * Cannot be cancelled, and will not report errors, simply returns null if
821 * the process ended abnormally.
824 * The parameters of the command, passed to
825 * {@link ProcessBuilder}
826 * @return The command output as a string
829 protected @Nullable String
getOutputFromCommand(List
<String
> command
) {
830 LOGGER
.info(() -> LOG_RUNNING_MESSAGE
+ ' ' + command
.toString());
831 List
<String
> lines
= ProcessUtils
.getOutputFromCommand(command
);
835 return String
.join("", lines
); //$NON-NLS-1$
839 * Get the results of invoking the specified command.
841 * The result should start with '{"results":...', as specified by the LAMI
842 * JSON protocol. The JSON itself may be split over multiple lines.
845 * The command to run (program and its arguments)
847 * The progress monitor
848 * @return The analysis results
849 * @throws CoreException
850 * If the command ended abnormally, and normal results were not
854 protected String
getResultsFromCommand(List
<String
> command
, IProgressMonitor monitor
)
855 throws CoreException
{
856 List
<String
> lines
= ProcessUtils
.getOutputFromCommandCancellable(command
, monitor
, nullToEmptyString(Messages
.LamiAnalysis_MainTaskName
), OUTPUT_READER
);
857 return checkNotNull(String
.join("", lines
)); //$NON-NLS-1$
860 private static final OutputReaderFunction OUTPUT_READER
= (reader
, monitor
) -> {
861 double workedSoFar
= 0.0;
863 String line
= reader
.readLine();
864 while (line
!= null && !line
.matches("\\s*\\{.*")) { //$NON-NLS-1$
866 * This is a line indicating progress, it has the form:
868 * 0.123 3000 of 5000 events processed
870 * The first part indicates the estimated fraction (out of 1.0) of
871 * work done. The second part is status text.
874 // Trim the line first to make sure the first character is
878 // Split at the first space
879 String
[] elems
= line
.split(" ", 2); //$NON-NLS-1$
881 if (elems
[0].matches("\\d.*")) { //$NON-NLS-1$
882 // It looks like we have a progress indication
884 // Try parsing the number
885 double cumulativeWork
= Double
.parseDouble(elems
[0]) * 1000;
886 double workedThisLoop
= cumulativeWork
- workedSoFar
;
888 // We're going backwards? Do not update the
890 if (workedThisLoop
> 0) {
891 monitor
.internalWorked(workedThisLoop
);
892 workedSoFar
= cumulativeWork
;
895 // There is a message: update the monitor's task name
896 if (elems
.length
>= 2) {
897 monitor
.setTaskName(elems
[1].trim());
899 } catch (NumberFormatException e
) {
900 // Continue reading progress lines anyway
904 line
= reader
.readLine();
907 List
<String
> results
= new ArrayList
<>();
908 while (line
!= null) {
910 * We have seen the first line containing a '{', this is our JSON
914 line
= reader
.readLine();
921 public @NonNull String
getName() {
926 public boolean isUserDefined() {
927 return fIsUserDefined
;