ros: add message flow analysis

From the ROS queues view, the user can select a message from a queue,
which will trigger the message flow analysis. This analysis computes
the path of that message through ROS nodes, including pub/sub queues
and callbacks. It depends on three analyses:

1. (Existing) queues analysis, for all the information about the
   state of queues and callbacks.
2. Connections model, which provides a list of ROS connections (e.g.
   pub/sub) between nodes. Note: services are not supported yet.
3. Messages transport analysis, itself depending on the connections
   model. It uses this model along with a ROS-specific extension of
   TmfEventMatching to try to link every message that gets sent over
   the network (by a publisher) to a message that is received on the
   other side (by a subscriber) in order to build a flow graph.

Its current limitations, which should eventually be addressed, are:

1. At this moment, it assumes that the flow graph is linear, i.e. one
   input message in a callback leads to, at most, one published
   message.
2. It currently only builds a graph for the selected message, and only
   builds it going forward.

Eventually, this should be used/extended to create a ROS-aware
critical path analysis.

Change-Id: I2effd8d74143f5431b713cb034119746fa7708b5
Signed-off-by: Christophe Bedard <bedard.christophe@gmail.com>
Reviewed-on: https://git.eclipse.org/r/140644
Tested-by: CI Bot
Reviewed-by: Genevieve Bastien <gbastien+lttng@versatic.net>
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/META-INF/MANIFEST.MF b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/META-INF/MANIFEST.MF
index 64ddd0c..102b814 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/META-INF/MANIFEST.MF
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/META-INF/MANIFEST.MF
@@ -22,13 +22,20 @@
 Export-Package: org.eclipse.tracecompass.incubator.internal.ros.core;x-friends:="org.eclipse.tracecompass.incubator.ros.core.tests,org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis.connections;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
+ org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
+ org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis.nodes;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis.queues;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis.tasks;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.analysis.timers;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
+ org.eclipse.tracecompass.incubator.internal.ros.core.signals;x-friends:="org.eclipse.tracecompass.incubator.ros.ui",
  org.eclipse.tracecompass.incubator.internal.ros.core.trace;x-friends:="org.eclipse.tracecompass.incubator.ros.ui,org.eclipse.tracecompass.incubator.ros.core.tests",
  org.eclipse.tracecompass.incubator.internal.ros.core.trace.layout
 Import-Package: com.google.common.base,
  com.google.common.collect,
- org.apache.commons.lang3
+ com.google.common.hash,
+ org.apache.commons.lang3,
+ org.eclipse.tracecompass.internal.lttng2.kernel.core,
+ org.eclipse.tracecompass.internal.lttng2.kernel.core.event.matching,
+ org.eclipse.tracecompass.internal.lttng2.kernel.core.trace.layout
 Automatic-Module-Name: org.eclipse.tracecompass.incubator.ros.core
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.properties b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.properties
index d2b5baf..a5e80f4 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.properties
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.properties
@@ -10,7 +10,8 @@
 Bundle-Vendor = Eclipse Trace Compass Incubator
 Bundle-Name = Trace Compass ROS Core Plug-in (Incubator)
 
-tracetype.type.ros = ROS Trace
+tracetype.type.ros = ROS Trace (incubator)
+tracetype.experiment.ros = ROS Experiment (incubator)
 
 analysis.ros = ROS (Incubator)
 analysis.ros.nodes = ROS Nodes
@@ -18,3 +19,7 @@
 analysis.ros.tasks = ROS Tasks
 analysis.ros.connections = ROS Connections
 analysis.ros.timers = ROS Timers
+
+analysis.ros.model.connections = ROS Connections Model
+analysis.ros.model.messagestransport = ROS Messages Transport Model
+analysis.ros.messageflow = ROS Message Flow
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.xml b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.xml
index 75510a0..f040054 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.xml
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/plugin.xml
@@ -11,6 +11,11 @@
             name="%tracetype.type.ros"
             trace_type="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosTrace">
       </type>
+      <experiment
+            experiment_type="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment"
+            id="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment"
+            name="%tracetype.experiment.ros">
+      </experiment>
    </extension>
    <extension
          point="org.eclipse.linuxtools.tmf.core.analysis">
@@ -69,6 +74,36 @@
                class="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosTrace">
          </tracetype>
       </module>
+      <module
+            analysis_module="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnectionsModelAnalysis"
+            automatic="false"
+            icon="icons/ros.png"
+            id="org.eclipse.tracecompass.incubator.ros.core.analysis.model.connections"
+            name="%analysis.ros.model.connections">
+         <tracetype
+               class="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment">
+         </tracetype>
+      </module>
+      <module
+            analysis_module="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport.RosMessagesTransportModelAnalysis"
+            icon="icons/ros.png"
+            id="org.eclipse.tracecompass.incubator.ros.core.analysis.model.messagestransport"
+            name="%analysis.ros.model.messagestransport">
+         <tracetype
+               class="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment">
+         </tracetype>
+      </module>
+      <module
+            analysis_module="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowAnalysis"
+            applies_experiment="true"
+            automatic="false"
+            icon="icons/ros.png"
+            id="org.eclipse.tracecompass.incubator.ros.core.analysis.messageflow"
+            name="%analysis.ros.messageflow">
+         <tracetype
+               class="org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment">
+         </tracetype>
+      </module>
    </extension>
    <extension
          point="org.eclipse.tracecompass.tmf.core.dataprovider">
@@ -92,6 +127,10 @@
             class="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.timers.RosTimersDataProviderFactory"
             id="org.eclipse.tracecompass.incubator.ros.core.analysis.timers.dataprovider">
       </dataProviderFactory>
+      <dataProviderFactory
+            class="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowDataProviderFactory"
+            id="org.eclipse.tracecompass.incubator.ros.core.analysis.messageflow.dataprovider">
+      </dataProviderFactory>
    </extension>
 
 </plugin>
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/connections/RosConnectionsDataProvider.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/connections/RosConnectionsDataProvider.java
index 80358a6..002fc07 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/connections/RosConnectionsDataProvider.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/connections/RosConnectionsDataProvider.java
@@ -33,7 +33,6 @@
 import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphArrow;
 import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphRowModel;
 import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphState;
-import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphArrow;
 import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphEntryModel;
 import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphRowModel;
 import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphState;
@@ -42,7 +41,6 @@
 import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
 
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.TreeMultimap;
@@ -58,7 +56,8 @@
     /** Data provider suffix ID */
     public static final String SUFFIX = ".dataprovider"; //$NON-NLS-1$
 
-    private @NonNull RosConnectionsAnalysis fModule;
+    // TODO use for arrows
+    // private @NonNull RosConnectionsAnalysis fModule;
 
     /**
      * Constructor
@@ -70,20 +69,12 @@
      */
     public RosConnectionsDataProvider(@NonNull ITmfTrace trace, @NonNull RosConnectionsAnalysis analysisModule) {
         super(trace, analysisModule);
-        fModule = analysisModule;
+        // fModule = analysisModule;
     }
 
     @Override
     public @NonNull TmfModelResponse<@NonNull List<@NonNull ITimeGraphArrow>> fetchArrows(@NonNull TimeQueryFilter filter, @Nullable IProgressMonitor monitor) {
-        ITmfStateSystem ss = fModule.getStateSystem();
-        if (ss == null) {
-            return new TmfModelResponse<>(null, ITmfResponse.Status.FAILED, CommonStatusMessage.ANALYSIS_INITIALIZATION_FAILED);
-        }
-
-        List<@NonNull ITimeGraphArrow> arrows = Lists.newArrayList();
-        filter.getTimesRequested();
-        arrows.add(new TimeGraphArrow(0, 0, 0, 0));
-        return new TmfModelResponse<>(arrows, ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);
+        return new TmfModelResponse<>(null, ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);
     }
 
     @Override
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowAnalysis.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowAnalysis.java
new file mode 100644
index 0000000..11b9f58
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowAnalysis.java
@@ -0,0 +1,129 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;
+
+import java.util.Collections;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModelProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.RosModelUtils;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.IRosMessageFlowModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport.IRosMessagesTransportModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport.RosMessagesTransportModelAnalysis;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.queues.RosQueuesAnalysis;
+import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
+import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * ROS message flow analysis, which computes the path of a selected message
+ * through ROS nodes, including queues and callbacks
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowAnalysis extends TmfAbstractAnalysisModule implements IRosModelProvider<IRosMessageFlowModel> {
+
+    /** The ID of this analysis module */
+    public static final @NonNull String ID = "org.eclipse.tracecompass.incubator.ros.core.analysis.messageflow"; //$NON-NLS-1$
+    /** Name for the {@link TargetMessageInfo} parameter */
+    public static final @NonNull String TARGET_MESSAGE_INFO_KEY = "target.message.info"; //$NON-NLS-1$
+
+    private @Nullable IProgressMonitor fMonitor;
+    private @Nullable RosMessageFlowModel fModel = null;
+
+    /**
+     * Constructor
+     */
+    public RosMessageFlowAnalysis() {
+        super();
+        setId(ID);
+        addParameter(TARGET_MESSAGE_INFO_KEY);
+    }
+
+    @Override
+    public @Nullable IRosMessageFlowModel getModel() {
+        return fModel;
+    }
+
+    @Override
+    protected boolean executeAnalysis(@NonNull IProgressMonitor monitor) {
+        // Target message info
+        // Do not execute if it has not been set
+        Object info = getParameter(TARGET_MESSAGE_INFO_KEY);
+        if (info == null || !(info instanceof TargetMessageInfo)) {
+            Activator.getInstance().logError("No target message set; not executing!"); //$NON-NLS-1$
+            return false;
+        }
+        TargetMessageInfo targetMessageInfo = (TargetMessageInfo) info;
+
+        ITmfTrace trace = getTrace();
+        if (!(trace instanceof TmfExperiment)) {
+            throw new IllegalStateException();
+        }
+        // Queues
+        @Nullable
+        ITmfStateSystem queuesStateSystem = RosModelUtils.getStateSystemFromAnalysis(trace, RosQueuesAnalysis.class, RosQueuesAnalysis.getFullAnalysisId());
+        if (queuesStateSystem == null) {
+            return false;
+        }
+        // Messages transport model
+        @Nullable
+        IRosMessagesTransportModel messagesTransportModel = RosModelUtils.getModelFromAnalysis(trace, RosMessagesTransportModelAnalysis.class, RosMessagesTransportModelAnalysis.ID);
+        if (messagesTransportModel == null) {
+            return false;
+        }
+
+        RosMessageFlowModel model = new RosMessageFlowModel(targetMessageInfo, queuesStateSystem, messagesTransportModel);
+        model.generateModel();
+        fModel = model;
+        return fModel.isModelDone() && !monitor.isCanceled();
+    }
+
+    @Override
+    protected void parameterChanged(@NonNull String name) {
+        if (name.equals(TARGET_MESSAGE_INFO_KEY)) {
+            // Re-run analysis
+            cancel();
+            resetAnalysis();
+            schedule();
+        }
+    }
+
+    @Override
+    protected @NonNull Iterable<@NonNull IAnalysisModule> getDependentAnalyses() {
+        ITmfTrace trace = getTrace();
+        if (trace == null) {
+            return Collections.emptySet();
+        }
+        RosQueuesAnalysis queues = TmfTraceUtils.getAnalysisModuleOfClass(trace, RosQueuesAnalysis.class, RosQueuesAnalysis.getFullAnalysisId());
+        RosMessagesTransportModelAnalysis messagesTransportModelAnalysis = TmfTraceUtils.getAnalysisModuleOfClass(trace, RosMessagesTransportModelAnalysis.class, RosMessagesTransportModelAnalysis.ID);
+        if (queues == null || messagesTransportModelAnalysis == null) {
+            return Collections.emptySet();
+        }
+        return ImmutableSet.of(queues, messagesTransportModelAnalysis);
+    }
+
+    @Override
+    protected void canceling() {
+        IProgressMonitor mon = fMonitor;
+        if ((mon != null) && (!mon.isCanceled())) {
+            mon.setCanceled(true);
+        }
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProvider.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProvider.java
new file mode 100644
index 0000000..bbb40de
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProvider.java
@@ -0,0 +1,237 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.IRosMessageFlowModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment.SegmentType;
+import org.eclipse.tracecompass.internal.tmf.core.model.AbstractTmfTraceDataProvider;
+import org.eclipse.tracecompass.internal.tmf.core.model.filters.TimeGraphStateQueryFilter;
+import org.eclipse.tracecompass.tmf.core.model.CommonStatusMessage;
+import org.eclipse.tracecompass.tmf.core.model.filters.SelectionTimeQueryFilter;
+import org.eclipse.tracecompass.tmf.core.model.filters.TimeQueryFilter;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphArrow;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphDataProvider;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphRowModel;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphState;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphArrow;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphEntryModel;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphRowModel;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphState;
+import org.eclipse.tracecompass.tmf.core.response.ITmfResponse;
+import org.eclipse.tracecompass.tmf.core.response.ITmfResponse.Status;
+import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+/**
+ * Data provider for the ROS Message Flow view
+ *
+ * @author Christophe Bedard
+ */
+@SuppressWarnings("restriction")
+public class RosMessageFlowDataProvider extends AbstractTmfTraceDataProvider implements ITimeGraphDataProvider<@NonNull TimeGraphEntryModel> {
+
+    /** Data provider suffix ID */
+    public static final @NonNull String SUFFIX = ".dataprovider"; //$NON-NLS-1$
+
+    /** Separator for info in entry model name */
+    public static final @NonNull String SEGMENT_NAME_SEP = ";"; //$NON-NLS-1$
+
+    private static final AtomicLong ATOMIC_LONG = new AtomicLong();
+
+    private @NonNull IRosMessageFlowModel fModel;
+
+    /**
+     * Map for message flow segment <-> id
+     * TODO eventually unflatten this tree
+     */
+    private final BiMap<RosMessageFlowSegment, Long> fSegmentToId = HashBiMap.create();
+
+    /**
+     * Constructor
+     *
+     * @param trace
+     *            the trace for this provider
+     * @param model
+     *            the message flow model
+     */
+    public RosMessageFlowDataProvider(@NonNull ITmfTrace trace, @NonNull IRosMessageFlowModel model) {
+        super(trace);
+        fModel = model;
+    }
+
+    private long getSegmentId(RosMessageFlowSegment segment) {
+        return fSegmentToId.computeIfAbsent(segment, i -> ATOMIC_LONG.getAndIncrement());
+    }
+
+    @Override
+    public @NonNull TmfModelResponse<@NonNull List<@NonNull TimeGraphEntryModel>> fetchTree(
+            @NonNull TimeQueryFilter filter, @Nullable IProgressMonitor monitor) {
+        if (!fModel.isModelDone()) {
+            return new TmfModelResponse<>(null, Status.RUNNING, CommonStatusMessage.RUNNING);
+        }
+
+        List<@NonNull TimeGraphEntryModel> entries = new ArrayList<>();
+        long rootId = ATOMIC_LONG.getAndIncrement();
+        entries.add(new TimeGraphEntryModel(rootId, -1, String.valueOf(getTrace().getName()), filter.getStart(), filter.getEnd()));
+        RosMessageFlowSegment firstSegment = fModel.getFirstSegment();
+        addTreeChildren(entries, firstSegment, rootId);
+        return new TmfModelResponse<>(entries, Status.COMPLETED, CommonStatusMessage.COMPLETED);
+    }
+
+    private void addTreeChildren(List<@NonNull TimeGraphEntryModel> entries, RosMessageFlowSegment segment, long parentId) {
+        // TODO eventually find a better way to represent this (not flat/similar
+        // to queues view)
+        long entryId = getSegmentId(segment);
+        entries.add(new RosMessageFlowSegmentEntryModel(entryId, parentId, segment.getStartTime(), segment.getEndTime(), segment));
+
+        Collection<RosMessageFlowSegment> next = segment.getNext();
+        for (RosMessageFlowSegment n : next) {
+            addTreeChildren(entries, n, parentId);
+        }
+    }
+
+    @Override
+    public @NonNull TmfModelResponse<@NonNull List<@NonNull ITimeGraphRowModel>> fetchRowModel(
+            @NonNull SelectionTimeQueryFilter filter, @Nullable IProgressMonitor monitor) {
+        if (!fModel.isModelDone()) {
+            return new TmfModelResponse<>(null, Status.RUNNING, CommonStatusMessage.RUNNING);
+        }
+
+        @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull String>>> predicates = new HashMap<>();
+        if (filter instanceof TimeGraphStateQueryFilter) {
+            TimeGraphStateQueryFilter timeEventFilter = (TimeGraphStateQueryFilter) filter;
+            predicates.putAll(computeRegexPredicate(timeEventFilter));
+        }
+
+        List<@NonNull ITimeGraphRowModel> rows = new ArrayList<>();
+        RosMessageFlowSegment firstSegment = fModel.getFirstSegment();
+        addRowModels(rows, firstSegment, predicates, monitor);
+        return new TmfModelResponse<>(rows, Status.COMPLETED, CommonStatusMessage.COMPLETED);
+    }
+
+    private void addRowModels(
+            List<@NonNull ITimeGraphRowModel> rows,
+            RosMessageFlowSegment segment,
+            @NonNull Map<@NonNull Integer, @NonNull Predicate<@NonNull Multimap<@NonNull String, @NonNull String>>> predicates,
+            @Nullable IProgressMonitor monitor) {
+        @NonNull List<@NonNull ITimeGraphState> eventList = new ArrayList<>();
+
+        // TODO eventually find a better way
+        long entryId = getSegmentId(segment);
+        long startTime = segment.getStartTime();
+        long duration = segment.getEndTime() - startTime + 1;
+        TimeGraphState state = new TimeGraphState(startTime, duration, getMatchingSegmentState(segment.getType()));
+        applyFilterAndAddState(eventList, state, entryId, predicates, monitor);
+        rows.add(new TimeGraphRowModel(entryId, eventList));
+
+        Collection<RosMessageFlowSegment> next = segment.getNext();
+        for (RosMessageFlowSegment n : next) {
+            addRowModels(rows, n, predicates, monitor);
+        }
+    }
+
+    private static int getMatchingSegmentState(SegmentType type) {
+        // See RosMessageFlowPresentationProvider.State
+        switch (type) {
+        case PUB_QUEUE:
+            return 0;
+        case SUB_QUEUE:
+            return 1;
+        case SUB_CALLBACK:
+            return 2;
+        case INVALID:
+            break;
+        default:
+            break;
+        }
+        return 4;
+    }
+
+    @Override
+    public @NonNull TmfModelResponse<@NonNull List<@NonNull ITimeGraphArrow>> fetchArrows(@NonNull TimeQueryFilter filter, @Nullable IProgressMonitor monitor) {
+        if (!fModel.isModelDone()) {
+            return new TmfModelResponse<>(null, Status.RUNNING, CommonStatusMessage.RUNNING);
+        }
+
+        return new TmfModelResponse<>(getArrows(filter.getStart(), filter.getEnd()), ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);
+    }
+
+    private @Nullable List<@NonNull ITimeGraphArrow> getArrows(long startTime, long endTime) {
+        List<@NonNull ITimeGraphArrow> arrows = Lists.newArrayList();
+        RosMessageFlowSegment firstSegment = fModel.getFirstSegment();
+        addArrows(firstSegment, arrows, startTime, endTime);
+        return arrows;
+    }
+
+    private void addArrows(RosMessageFlowSegment parent, List<@NonNull ITimeGraphArrow> arrows, long startTime, long endTime) {
+        Collection<RosMessageFlowSegment> next = parent.getNext();
+        for (RosMessageFlowSegment n : next) {
+            if (parent.getEndTime() <= endTime && n.getStartTime() >= startTime) {
+                addArrow(parent, n, arrows);
+            }
+            addArrows(n, arrows, startTime, endTime);
+        }
+    }
+
+    private void addArrow(RosMessageFlowSegment parent, RosMessageFlowSegment child, List<@NonNull ITimeGraphArrow> arrows) {
+        long sourceId = getSegmentId(parent);
+        long destinationId = getSegmentId(child);
+        long time = parent.getEndTime();
+        long duration = child.getStartTime() - parent.getEndTime();
+        arrows.add(new TimeGraphArrow(sourceId, destinationId, time, duration, getMatchingArrowState(parent.getType(), child.getType())));
+    }
+
+    private static int getMatchingArrowState(SegmentType source, SegmentType destination) {
+        // See RosMessageFlowPresentationProvider.State
+        if (source.equals(SegmentType.PUB_QUEUE) && destination.equals(SegmentType.SUB_QUEUE)) {
+            return 4;
+        } else if (source.equals(SegmentType.SUB_QUEUE) && destination.equals(SegmentType.SUB_CALLBACK)) {
+            return 5;
+        } else if (destination.equals(SegmentType.PUB_QUEUE)) {
+            return 6;
+        }
+        return 7;
+    }
+
+    @Override
+    public @NonNull TmfModelResponse<@NonNull Map<@NonNull String, @NonNull String>> fetchTooltip(@NonNull SelectionTimeQueryFilter filter, @Nullable IProgressMonitor monitor) {
+        // TODO
+        return new TmfModelResponse<>(null, ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);
+    }
+
+    @Override
+    public @NonNull String getId() {
+        return getFullDataProviderId();
+    }
+
+    /**
+     * @return the full dataprovider ID
+     */
+    public static @NonNull String getFullDataProviderId() {
+        return RosMessageFlowAnalysis.ID + SUFFIX;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProviderFactory.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProviderFactory.java
new file mode 100644
index 0000000..97aee31
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowDataProviderFactory.java
@@ -0,0 +1,84 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.IRosMessageFlowModel;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.dataprovider.IDataProviderFactory;
+import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel;
+import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataProvider;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
+import org.eclipse.tracecompass.tmf.core.signal.TmfStartAnalysisSignal;
+import org.eclipse.tracecompass.tmf.core.signal.TmfTraceClosedSignal;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+
+/**
+ * {@link RosMessageFlowDataProvider} factory
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowDataProviderFactory implements IDataProviderFactory {
+
+    private final Map<ITmfTrace, RosMessageFlowAnalysis> fMap = new HashMap<>();
+
+    /**
+     * Constructor, registers the module with the {@link TmfSignalManager}
+     */
+    public RosMessageFlowDataProviderFactory() {
+        TmfSignalManager.register(this);
+    }
+
+    @Override
+    public @Nullable ITmfTreeDataProvider<? extends ITmfTreeDataModel> createProvider(@NonNull ITmfTrace trace) {
+        RosMessageFlowAnalysis module = fMap.remove(trace);
+        if (module == null) {
+            return null;
+        }
+        IRosMessageFlowModel model = module.getModel();
+        if (model == null) {
+            return null;
+        }
+        return new RosMessageFlowDataProvider(trace, model);
+    }
+
+    /**
+     * {@link TmfSignalHandler} for when {@link RosMessageFlowAnalysis} is
+     * started, as the analysis is not registered with the trace, we use this to
+     * know to associate a {@link RosMessageFlowAnalysis} to a trace.
+     *
+     * @param startAnalysisSignal
+     *            analysis started signal
+     */
+    @TmfSignalHandler
+    public synchronized void analysisStarted(TmfStartAnalysisSignal startAnalysisSignal) {
+        IAnalysisModule module = startAnalysisSignal.getAnalysisModule();
+        if (module instanceof RosMessageFlowAnalysis) {
+            RosMessageFlowAnalysis analysis = (RosMessageFlowAnalysis) module;
+            fMap.put(analysis.getTrace(), analysis);
+        }
+    }
+
+    /**
+     * Remove the closed trace's analysis to avoid resource leaks
+     *
+     * @param traceClosedSignal
+     *            the TMF trace closed signal
+     */
+    @TmfSignalHandler
+    public synchronized void traceClosed(TmfTraceClosedSignal traceClosedSignal) {
+        fMap.remove(traceClosedSignal.getTrace());
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowSegmentEntryModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowSegmentEntryModel.java
new file mode 100644
index 0000000..c5be10d
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/RosMessageFlowSegmentEntryModel.java
@@ -0,0 +1,51 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.TimeGraphEntryModel;
+
+/**
+ * Extension of {@link TimeGraphEntryModel} to keep a reference to the
+ * corresponding {@link RosMessageFlowSegment}
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowSegmentEntryModel extends TimeGraphEntryModel {
+
+    private final RosMessageFlowSegment fSegment;
+
+    /**
+     * Constructor
+     *
+     * @param id
+     *            Entry ID
+     * @param parentId
+     *            Parent ID
+     * @param startTime
+     *            Start time
+     * @param endTime
+     *            End time
+     * @param segment
+     *            the corresponding segment
+     */
+    public RosMessageFlowSegmentEntryModel(long id, long parentId, long startTime, long endTime, RosMessageFlowSegment segment) {
+        super(id, parentId, StringUtils.EMPTY, startTime, endTime);
+        fSegment = segment;
+    }
+
+    /**
+     * @return the segment
+     */
+    public RosMessageFlowSegment getSegment() {
+        return fSegment;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/TargetMessageInfo.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/TargetMessageInfo.java
new file mode 100644
index 0000000..7974d5a
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/messageflow/TargetMessageInfo.java
@@ -0,0 +1,131 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow;
+
+/**
+ * Represents a target message to be followed. This contains the necessary
+ * information to find the actual corresponding message.
+ *
+ * @author Christophe Bedard
+ */
+public class TargetMessageInfo {
+
+    private final String fNode;
+    private final String fTopic;
+    private final RosQueueType fQueueType;
+    private final int fQueuePosition;
+    private final long fMsgRef;
+    private final long fSelectedTimestamp;
+    private final long fMsgTimestamp;
+
+    /**
+     * Type of the queue for the selected message
+     *
+     * @author Christophe Bedard
+     */
+    public enum RosQueueType {
+        /**
+         * Subscriber queue
+         */
+        SUB,
+        /**
+         * Publisher queue
+         */
+        PUB
+    }
+
+    /**
+     * Constructor
+     *
+     * @param node
+     *            the node name
+     * @param topic
+     *            the topic name
+     * @param queueType
+     *            the type of the queue in which this message is
+     * @param queuePosition
+     *            the message's position in the queue
+     * @param msgRef
+     *            the message reference (0x0123abc)
+     * @param selectedTimestamp
+     *            the selected timestamp
+     * @param msgTimestamp
+     *            the selected message's begin timestamp
+     */
+    public TargetMessageInfo(String node, String topic, RosQueueType queueType, int queuePosition, String msgRef, long selectedTimestamp, long msgTimestamp) {
+        fNode = node;
+        fTopic = topic;
+        fQueueType = queueType;
+        fQueuePosition = queuePosition;
+        fMsgRef = Long.decode(msgRef);
+        fSelectedTimestamp = selectedTimestamp;
+        fMsgTimestamp = msgTimestamp;
+    }
+
+    /**
+     * @return the node name
+     */
+    public String getNode() {
+        return fNode;
+    }
+
+    /**
+     * @return the topic name
+     */
+    public String getTopic() {
+        return fTopic;
+    }
+
+    /**
+     * @return the type of the queue in which this message is
+     */
+    public RosQueueType getQueueType() {
+        return fQueueType;
+    }
+
+    /**
+     * @return the message's position in the queue
+     */
+    public int getQueuePosition() {
+        return fQueuePosition;
+    }
+
+    /**
+     * @return the message reference
+     */
+    public long getMsgRef() {
+        return fMsgRef;
+    }
+
+    /**
+     * @return the selected timestamp
+     */
+    public long getSelectedTimestamp() {
+        return fSelectedTimestamp;
+    }
+
+    /**
+     * @return the selected message's begin timestamp
+     */
+    public long getMsgTimestamp() {
+        return fMsgTimestamp;
+    }
+
+    @Override
+    public String toString() {
+        return "TargetMessageInfo: " //$NON-NLS-1$
+                + fNode + ", " //$NON-NLS-1$
+                + fTopic + "(" + fQueueType.name() + "):"  //$NON-NLS-1$//$NON-NLS-2$
+                + "0x" + Long.toHexString(fMsgRef) //$NON-NLS-1$
+                + " @ " + fQueuePosition //$NON-NLS-1$
+                + ", " + fMsgTimestamp //$NON-NLS-1$
+                + " | " + fSelectedTimestamp; //$NON-NLS-1$
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModel.java
new file mode 100644
index 0000000..be4e67e
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModel.java
@@ -0,0 +1,23 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model;
+
+/**
+ * Interface for ROS models
+ *
+ * @author Christophe Bedard
+ */
+public interface IRosModel {
+
+    /**
+     * Launch the process of generating the model
+     */
+    void generateModel();
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModelProvider.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModelProvider.java
new file mode 100644
index 0000000..66c1f09
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/IRosModelProvider.java
@@ -0,0 +1,30 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Interface for a model provider, which is an analysis that takes information
+ * from other model(s), existing state system(s), and/or trace event(s), and
+ * generates a model
+ *
+ * @param <M>
+ *            the ROS model it provides
+ * @author Christophe Bedard
+ */
+public interface IRosModelProvider<M extends IRosModel> {
+
+    /**
+     * @return the model, or {@code null} if it hasn't been generated yet
+     */
+    @Nullable
+    M getModel();
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/RosModelUtils.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/RosModelUtils.java
new file mode 100644
index 0000000..7d838e4
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/RosModelUtils.java
@@ -0,0 +1,98 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.statesystem.TmfStateSystemAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
+
+/**
+ * Utils for ROS models (analyses and models themselves)
+ *
+ * @author Christophe Bedard
+ */
+public class RosModelUtils {
+
+    /**
+     * Get model from analysis after scheduling and waiting for it to complete
+     *
+     * @param trace
+     *            the trace from which to get the analysis and model
+     * @param moduleClass
+     *            the analysis module class
+     * @param id
+     *            the ID of the analysis module
+     * @param <M>
+     *            the model
+     * @param <P>
+     *            the analysis that builds/provides the model
+     * @return the model, or {@code null} if it failed
+     */
+    public static <M extends IRosModel, P extends IRosModelProvider<M> & IAnalysisModule> @Nullable M getModelFromAnalysis(
+            @NonNull ITmfTrace trace,
+            @NonNull Class<@NonNull P> moduleClass,
+            @NonNull String id) {
+        @Nullable
+        P modelAnalysis = TmfTraceUtils.getAnalysisModuleOfClass(trace, moduleClass, id);
+        if (modelAnalysis == null) {
+            throw new IllegalStateException("There should be an analysis for this class"); //$NON-NLS-1$
+        }
+        modelAnalysis.schedule();
+        if (!modelAnalysis.waitForCompletion()) {
+            throw new IllegalStateException("Problem waiting for the analysis"); //$NON-NLS-1$
+        }
+        @Nullable
+        M model = modelAnalysis.getModel();
+        if (model == null) {
+            Activator.getInstance().logError("Model is null"); //$NON-NLS-1$
+        }
+        return model;
+    }
+
+    /**
+     * Get state system from analysis after scheduling and waiting for it to
+     * complete
+     *
+     * @param trace
+     *            the trace from which to get the analysis and model
+     * @param moduleClass
+     *            the analysis module class
+     * @param id
+     *            the ID of the analysis module
+     * @param <A>
+     *            the analysis that provides the state system
+     * @return the state system, or {@code null} if it failed
+     */
+    public static <A extends TmfStateSystemAnalysisModule> @Nullable ITmfStateSystem getStateSystemFromAnalysis(
+            @NonNull ITmfTrace trace,
+            @NonNull Class<@NonNull A> moduleClass,
+            @NonNull String id) {
+        @Nullable
+        A analysis = TmfTraceUtils.getAnalysisModuleOfClass(trace, moduleClass, id);
+        if (analysis == null) {
+            throw new IllegalStateException("There should be an analysis for this class"); //$NON-NLS-1$
+        }
+        analysis.schedule();
+        if (!analysis.waitForCompletion()) {
+            throw new IllegalStateException("Problem waiting for the analysis"); //$NON-NLS-1$
+        }
+        @Nullable
+        ITmfStateSystem ss = analysis.getStateSystem();
+        if (ss == null) {
+            Activator.getInstance().logError("State system is null"); //$NON-NLS-1$
+        }
+        return ss;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/ConnectionEndpoint.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/ConnectionEndpoint.java
new file mode 100644
index 0000000..623b136
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/ConnectionEndpoint.java
@@ -0,0 +1,123 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Represents a connection endpoint with an address and a port. IPv4-specific
+ * for now.
+ *
+ * @author Christophe Bedard
+ */
+public class ConnectionEndpoint {
+
+    private static final @NonNull String ADDRESS_SEP = "."; //$NON-NLS-1$
+    private static final @NonNull String PORT_SEP = ":"; //$NON-NLS-1$
+
+    private final long[] fAddress;
+    private final long fPort;
+
+    /**
+     * Constructor
+     *
+     * @param address
+     *            the address
+     * @param port
+     *            the port
+     */
+    public ConnectionEndpoint(long[] address, long port) {
+        fAddress = address;
+        fPort = port;
+    }
+
+    /**
+     * @return the address
+     */
+    public long[] getAddress() {
+        return fAddress;
+    }
+
+    /**
+     * @return the port
+     */
+    public long getPort() {
+        return fPort;
+    }
+
+    /**
+     * Format address and port
+     *
+     * @param address
+     *            the address
+     * @param port
+     *            the port
+     * @return the address and port formated like 1.2.3.4:1234
+     */
+    public static String formatAddressPort(long[] address, long port) {
+        return StringUtils.join(ArrayUtils.toObject(address), ADDRESS_SEP) + PORT_SEP + String.valueOf(port);
+    }
+
+    @Override
+    public String toString() {
+        return formatAddressPort(fAddress, fPort);
+    }
+
+    /**
+     * Create a {@link ConnectionEndpoint} from a string like "1.2.3.4:1234"
+     *
+     * @param f
+     *            the string
+     * @return the {@link ConnectionEndpoint}, or {@code null} if invalid
+     */
+    public static @Nullable ConnectionEndpoint fromStringFormat(String f) {
+        @NonNull String[] split = f.split(PORT_SEP);
+        if (split.length != 2) {
+            return null;
+        }
+
+        @NonNull String[] addressStr = split[0].split(Objects.requireNonNull(Pattern.quote(ADDRESS_SEP)));
+        long[] address = new long[addressStr.length];
+        for (int i = 0; i < addressStr.length; ++i) {
+            address[i] = Long.parseLong(addressStr[i]);
+        }
+        long port = Long.parseLong(split[1]);
+
+        return new ConnectionEndpoint(address, port);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + Objects.hash(fPort);
+        result = prime * result + Arrays.hashCode(fAddress);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof ConnectionEndpoint)) {
+            return false;
+        }
+        ConnectionEndpoint other = (ConnectionEndpoint) obj;
+        return Arrays.equals(fAddress, other.fAddress) && fPort == other.fPort;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/IRosConnectionsModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/IRosConnectionsModel.java
new file mode 100644
index 0000000..f8d7c1b
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/IRosConnectionsModel.java
@@ -0,0 +1,82 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModel;
+
+/**
+ * Interface that represents the model for ROS connections, which are network
+ * connections between publishers and subscribers
+ *
+ * @author Christophe Bedard
+ */
+public interface IRosConnectionsModel extends IRosModel {
+
+    /**
+     * Get all connections
+     *
+     * @return an unmodifiable view of the {@link RosConnection}s, or
+     *         {@code null} if it hasn't been created
+     */
+    @Nullable
+    Collection<RosConnection> getConnections();
+
+    /**
+     * Get a node's connections
+     *
+     * @param nodeName
+     *            the node name
+     * @return the node's {@link RosConnection}s
+     */
+    List<RosConnection> getNodeConnections(String nodeName);
+
+    /**
+     * Get connection corresponding to an endpoint and a topic name
+     *
+     * @param endpoint
+     *            the connection endpoint
+     * @param topicName
+     *            the topic name
+     * @return the corresponding {@link RosConnection}, or {@code null} if it
+     *         doesn't exist
+     */
+    @Nullable
+    RosConnection getConnection(ConnectionEndpoint endpoint, String topicName);
+
+    /**
+     * Get connection corresponding to a node and a published topic
+     *
+     * @param pubNodeName
+     *            the publisher node name
+     * @param topicName
+     *            the topic name
+     * @return the corresponding {@link RosConnection}, or {@code null} if it
+     *         doesn't exist
+     */
+    @Nullable
+    RosConnection getConnectionFromNodePublishingOnTopic(String pubNodeName, String topicName);
+
+    /**
+     * Get connection corresponding to a source+destination endpoints pair
+     *
+     * @param source
+     *            the source endpoint
+     * @param destination
+     *            the destination endpoint
+     * @return the corresponding {@link RosConnection}, or {@code null} if it
+     *         doesn't exist
+     */
+    @Nullable
+    RosConnection getConnectionBetweenEndpoints(ConnectionEndpoint source, ConnectionEndpoint destination);
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnection.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnection.java
new file mode 100644
index 0000000..ee49f6f
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnection.java
@@ -0,0 +1,83 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import java.util.Objects;
+
+/**
+ * Represents a connection between two ROS nodes. Right now this only applies to
+ * a publisher and a subscriber.
+ *
+ * @author Christophe Bedard
+ */
+public class RosConnection {
+
+    private final RosConnectionEndpoint fPub;
+    private final RosConnectionEndpoint fSub;
+
+    /**
+     * Constructor
+     *
+     * @param pub
+     *            the publisher's connection info
+     * @param sub
+     *            the subscriber's connection info
+     */
+    public RosConnection(RosConnectionEndpoint pub, RosConnectionEndpoint sub) {
+        fPub = pub;
+        fSub = sub;
+    }
+
+    /**
+     * @return the publisher connection
+     */
+    public RosConnectionEndpoint getPub() {
+        return fPub;
+    }
+
+    /**
+     * @return the subscriber connection
+     */
+    public RosConnectionEndpoint getSub() {
+        return fSub;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fPub, fSub);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof RosConnection)) {
+            return false;
+        }
+        RosConnection other = (RosConnection) obj;
+        return Objects.equals(fPub, other.fPub) && Objects.equals(fSub, other.fSub);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("RosConnection"); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\tPUB: " + fPub.toString()); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\tSUB: " + fSub.toString()); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        return b.toString();
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionEndpoint.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionEndpoint.java
new file mode 100644
index 0000000..bd4f145
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionEndpoint.java
@@ -0,0 +1,95 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import java.util.Objects;
+
+/**
+ * Represents one side of a connection between two ROS nodes
+ *
+ * TODO add connection end time
+ *
+ * @author Christophe Bedard
+ */
+public class RosConnectionEndpoint {
+
+    private final String fTopicName;
+    private final String fNodeName;
+    private final ConnectionEndpoint fEndpoint;
+    private final long fStartTime;
+
+    /**
+     * Constructor
+     *
+     * @param topic
+     *            the topic name
+     * @param node
+     *            the node name
+     * @param endpoint
+     *            the endpoint
+     * @param time
+     *            the connection start time
+     */
+    public RosConnectionEndpoint(String topic, String node, ConnectionEndpoint endpoint, long time) {
+        fTopicName = topic;
+        fNodeName = node;
+        fEndpoint = endpoint;
+        fStartTime = time;
+    }
+
+    /**
+     * @return the topic name
+     */
+    public String getTopicName() {
+        return fTopicName;
+    }
+
+    /**
+     * @return the node name
+     */
+    public String getNodeName() {
+        return fNodeName;
+    }
+
+    /**
+     * @return the network endpoint
+     */
+    public ConnectionEndpoint getEndpoint() {
+        return fEndpoint;
+    }
+
+    /**
+     * @return the connection start time
+     */
+    public long getStartTime() {
+        return fStartTime;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fTopicName, fNodeName, fEndpoint, fStartTime);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("\tRosConnectionEndpoint"); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\t\ttopic     =" + fTopicName); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\t\thost:port =" + fEndpoint.toString()); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\t\tnode      =" + fNodeName); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\t\ttime      =" + fStartTime); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        return b.toString();
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModel.java
new file mode 100644
index 0000000..0d7e02e
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModel.java
@@ -0,0 +1,234 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.connections.RosConnectionsStateProvider;
+import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
+import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
+import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
+import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
+import org.eclipse.tracecompass.tmf.core.util.Pair;
+
+import com.google.common.collect.Lists;
+
+/**
+ * {@link IRosConnectionsModel} implementation based on a complete connections
+ * analysis state system
+ *
+ * TODO support service connections
+ *
+ * @author Christophe Bedard
+ */
+public class RosConnectionsModel implements IRosConnectionsModel {
+
+    private static final String TYPE_PUB_LINK = "TransportPublisherLink"; //$NON-NLS-1$
+    private static final String TYPE_SUB_LINK = "TransportSubscriberLink"; //$NON-NLS-1$
+
+    private List<RosConnection> fConnections = null;
+    private final @NonNull ITmfStateSystem fSs;
+    private final @NonNull IProgressMonitor fMon;
+
+    /**
+     * Constructor
+     *
+     * @param stateSystem
+     *            the state system
+     * @param monitor
+     *            the progress monitor
+     */
+    public RosConnectionsModel(@NonNull ITmfStateSystem stateSystem, @NonNull IProgressMonitor monitor) {
+        fSs = stateSystem;
+        fMon = monitor;
+    }
+
+    @Override
+    public void generateModel() {
+        try {
+            List<RosConnectionEndpoint> connectionsInfoPub = new ArrayList<>();
+            // (Remote endpoint, topic name) --> local subscriber connection
+            // endpoint
+            Map<Pair<ConnectionEndpoint, String>, RosConnectionEndpoint> connectionsInfoSub = new HashMap<>();
+
+            if (fMon.isCanceled()) {
+                return;
+            }
+            gatherConnectionInfo(fSs, connectionsInfoPub, connectionsInfoSub);
+            if (fMon.isCanceled()) {
+                return;
+            }
+            fConnections = linkConnectionInfo(connectionsInfoPub, connectionsInfoSub);
+
+            if (connectionsInfoPub.size() != connectionsInfoSub.size()) {
+                Activator.getInstance().logWarning("Number of publisher connection info and subscriber connection info do not match!"); //$NON-NLS-1$
+            }
+        } catch (StateSystemDisposedException e) {
+            // Ignore
+        }
+    }
+
+    /**
+     * Go through state system and gather connection information (from each
+     * side).
+     *
+     * @param ss
+     *            the state system
+     * @param connectionsInfoPub
+     *            the list to put publisher connection endpoints into
+     * @param connectionsInfoSub
+     *            the map to put subscriber connection endpoints into
+     * @throws StateSystemDisposedException
+     * @throws TimeRangeException
+     * @throws IndexOutOfBoundsException
+     */
+    private static void gatherConnectionInfo(ITmfStateSystem ss, List<RosConnectionEndpoint> connectionsInfoPub, Map<Pair<ConnectionEndpoint, String>, RosConnectionEndpoint> connectionsInfoSub)
+            throws IndexOutOfBoundsException, TimeRangeException, StateSystemDisposedException {
+        // Gather info about all connections
+        List<@NonNull Integer> nodeQuarks = ss.getSubAttributes(ITmfStateSystem.ROOT_ATTRIBUTE, false);
+        for (@NonNull Integer nodeQuark : nodeQuarks) {
+            String nodeName = ss.getAttributeName(nodeQuark);
+            List<@NonNull Integer> typeQuarks = ss.getSubAttributes(nodeQuark, false);
+            for (@NonNull Integer typeQuark : typeQuarks) {
+                String type = ss.getAttributeName(typeQuark);
+                if (!(type.equals(TYPE_PUB_LINK) || type.equals(TYPE_SUB_LINK))) {
+                    // TODO support service connections
+                    continue;
+                }
+
+                // Publisher of the following topics if this is a subscriber
+                // link
+                boolean isPubSide = type.equals(TYPE_SUB_LINK);
+                List<@NonNull Integer> topicQuarks = ss.getSubAttributes(typeQuark, false);
+                for (@NonNull Integer topicQuark : topicQuarks) {
+                    String topicName = ss.getAttributeName(topicQuark);
+                    List<@NonNull Integer> connectionQuarks = ss.getSubAttributes(topicQuark, false);
+                    for (@NonNull Integer connectionQuark : connectionQuarks) {
+                        Iterable<@NonNull ITmfStateInterval> connectionIntervalsIter = ss.query2D(Collections.singleton(connectionQuark), ss.getStartTime(), ss.getCurrentEndTime());
+                        List<@NonNull ITmfStateInterval> connectionIntervals = Lists.newArrayList(connectionIntervalsIter);
+                        // First interval should be null, then the second
+                        // interval should have the host:port combo
+                        if (!(connectionIntervals.size() >= 1)) {
+                            continue;
+                        }
+                        ITmfStateInterval connectionInterval = connectionIntervals.get(1);
+                        String hostsPorts = connectionInterval.getValueString();
+                        // Local and remote host:port (in that order) with a
+                        // separator in between
+                        @NonNull String[] localRemote = hostsPorts.split(RosConnectionsStateProvider.CONNECTION_HOST_SEPARATOR);
+                        String localHostPort = localRemote[0];
+                        ConnectionEndpoint localEndpoint = ConnectionEndpoint.fromStringFormat(localHostPort);
+                        RosConnectionEndpoint local = new RosConnectionEndpoint(topicName, nodeName, localEndpoint, connectionInterval.getStartTime());
+                        if (isPubSide) {
+                            connectionsInfoPub.add(local);
+                        } else {
+                            String remoteHostPort = localRemote[1];
+                            ConnectionEndpoint remoteEndpoint = ConnectionEndpoint.fromStringFormat(remoteHostPort);
+                            connectionsInfoSub.put(new Pair<>(remoteEndpoint, topicName), local);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Link matching {@link RosConnectionEndpoint} to make up a
+     * {@link RosConnection}
+     *
+     * @param connectionsInfoPub
+     *            the list of publisher connection endpoints
+     * @param connectionsInfoSub
+     *            the subscriber connection info map
+     * @return the resulting list of {@link RosConnection}
+     */
+    private static List<RosConnection> linkConnectionInfo(List<RosConnectionEndpoint> connectionsInfoPub, Map<Pair<ConnectionEndpoint, String>, RosConnectionEndpoint> connectionsInfoSub) {
+        List<RosConnection> connections = new ArrayList<>();
+        for (RosConnectionEndpoint pubInfo : connectionsInfoPub) {
+            // Both of the following have to match, since the same
+            // Transport*Link can be used for two different pub/sub connections
+            // (i.e. the same host:port can be used for two different topics):
+            // - remote host:port on the subscriber's side == local host:port on
+            // the publisher's side
+            // - topic name on the subscriber's side == topic name on the
+            // publisher's side
+            RosConnectionEndpoint subInfo = connectionsInfoSub.get(new Pair<>(pubInfo.getEndpoint(), pubInfo.getTopicName()));
+            if (subInfo == null) {
+                Activator.getInstance().logError("No connection info match!"); //$NON-NLS-1$
+                continue;
+            }
+            connections.add(new RosConnection(pubInfo, subInfo));
+        }
+
+        return connections;
+    }
+
+    @Override
+    public @Nullable Collection<RosConnection> getConnections() {
+        return Collections.unmodifiableCollection(fConnections);
+    }
+
+    @Override
+    public List<RosConnection> getNodeConnections(String nodeName) {
+        return fConnections.stream().filter(c -> c.getPub().getNodeName().equals(nodeName) || c.getSub().getNodeName().equals(nodeName)).collect(Collectors.toList());
+    }
+
+    @Override
+    public @Nullable RosConnection getConnection(ConnectionEndpoint endpoint, String topicName) {
+        List<RosConnection> connections = fConnections.stream().filter(c -> (c.getPub().getEndpoint().equals(endpoint) || c.getSub().getEndpoint().equals(endpoint)) && c.getPub().getTopicName().equals(topicName)).collect(Collectors.toList());
+        // Since a host:port & topic name combo is unique, there should be at
+        // most one match
+        if (connections.size() > 1) {
+            Activator.getInstance().logError("Got more than 1 connection matching a host:port and topic name pair!"); //$NON-NLS-1$
+        }
+        return (!connections.isEmpty()) ? connections.get(0) : null;
+    }
+
+    @Override
+    public @Nullable RosConnection getConnectionFromNodePublishingOnTopic(String pubNodeName, String topicName) {
+        List<RosConnection> pubConnections = fConnections.stream().filter(c -> c.getPub().getNodeName().equals(pubNodeName) && c.getPub().getTopicName().equals(topicName)).collect(Collectors.toList());
+        // Should only be one match
+        if (pubConnections.size() > 1) {
+            Activator.getInstance().logError("Got more than 1 connection matching a node and published topic name pair!"); //$NON-NLS-1$
+        }
+        return (!pubConnections.isEmpty()) ? pubConnections.get(0) : null;
+    }
+
+    @Override
+    public @Nullable RosConnection getConnectionBetweenEndpoints(ConnectionEndpoint source, ConnectionEndpoint destination) {
+        List<RosConnection> connections = fConnections.stream().filter(c -> (c.getPub().getEndpoint().equals(source) && c.getSub().getEndpoint().equals(destination))).collect(Collectors.toList());
+        // There should only be one or none
+        if (connections.size() > 1) {
+            Activator.getInstance().logError("Got more than 1 connection matching a source+destination endpoints pair!"); //$NON-NLS-1$
+        }
+        return (!connections.isEmpty()) ? connections.get(0) : null;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("RosConnectionsModel"); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        for (RosConnection connection : fConnections) {
+            b.append("\t" + connection.toString()); //$NON-NLS-1$
+        }
+        return b.toString();
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModelAnalysis.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModelAnalysis.java
new file mode 100644
index 0000000..dc80eb2
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/connections/RosConnectionsModelAnalysis.java
@@ -0,0 +1,128 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections;
+
+import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.connections.RosConnectionsAnalysis;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.connections.RosConnectionsAnalysisRequirement;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModelProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.RosModelUtils;
+import org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosExperiment;
+import org.eclipse.tracecompass.incubator.internal.ros.core.trace.RosTrace;
+import org.eclipse.tracecompass.incubator.internal.ros.core.trace.layout.IRosEventLayout;
+import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.analysis.requirements.TmfAbstractAnalysisRequirement;
+import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
+import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * ROS Connections model analysis. Creates a {@link RosConnectionsModel} from
+ * the {@link RosConnectionsAnalysis} state system.
+ *
+ * @author Christophe Bedard
+ */
+public class RosConnectionsModelAnalysis extends TmfAbstractAnalysisModule implements IRosModelProvider<IRosConnectionsModel> {
+
+    /** The ID of this analysis module */
+    public static final @NonNull String ID = "org.eclipse.tracecompass.incubator.ros.core.analysis.model.connections"; //$NON-NLS-1$
+
+    /** The analysis requirements */
+    protected @Nullable Set<@NonNull TmfAbstractAnalysisRequirement> fAnalysisRequirements = null;
+
+    private @Nullable IProgressMonitor fMonitor;
+    private @Nullable RosConnectionsModel fModel = null;
+
+    /**
+     * Constructor
+     */
+    public RosConnectionsModelAnalysis() {
+        setId(ID);
+    }
+
+    @Override
+    public @Nullable RosConnectionsModel getModel() {
+        return fModel;
+    }
+
+    @Override
+    protected boolean executeAnalysis(@NonNull IProgressMonitor monitor) throws TmfAnalysisException {
+        fMonitor = monitor;
+        ITmfTrace trace = getTrace();
+        if (!(trace instanceof TmfExperiment)) {
+            throw new IllegalStateException();
+        }
+        // Connections
+        @Nullable ITmfStateSystem ss = RosModelUtils.getStateSystemFromAnalysis(trace, RosConnectionsAnalysis.class, RosConnectionsAnalysis.getFullAnalysisId());
+        if (ss == null) {
+            return false;
+        }
+
+        RosConnectionsModel model = new RosConnectionsModel(ss, monitor);
+        model.generateModel();
+        fModel = model;
+        return !monitor.isCanceled();
+    }
+
+    @Override
+    public @NonNull Iterable<@NonNull TmfAbstractAnalysisRequirement> getAnalysisRequirements() {
+        // Based on RosConnectionsAnalysis' requirements
+        Set<@NonNull TmfAbstractAnalysisRequirement> requirements = fAnalysisRequirements;
+        if (requirements == null) {
+            RosExperiment experiment = (RosExperiment) getTrace();
+            IRosEventLayout layout = IRosEventLayout.getDefault();
+            if (experiment != null) {
+                List<@NonNull ITmfTrace> traces = experiment.getTraces();
+                for (@NonNull ITmfTrace trace : traces) {
+                    if (trace instanceof RosTrace) {
+                        layout = ((RosTrace) trace).getEventLayout();
+                    }
+                }
+            }
+            requirements = ImmutableSet.of(new RosConnectionsAnalysisRequirement(checkNotNull(layout)));
+            fAnalysisRequirements = requirements;
+        }
+        return requirements;
+    }
+
+    @Override
+    protected @NonNull Iterable<@NonNull IAnalysisModule> getDependentAnalyses() {
+        ITmfTrace trace = getTrace();
+        if (trace == null) {
+            return Collections.emptySet();
+        }
+        RosConnectionsAnalysis connections = TmfTraceUtils.getAnalysisModuleOfClass(trace, RosConnectionsAnalysis.class, RosConnectionsAnalysis.getFullAnalysisId());
+        if (connections == null) {
+            throw new IllegalStateException("There should be an analysis for this class"); //$NON-NLS-1$
+        }
+        return Collections.singleton(connections);
+    }
+
+    @Override
+    protected void canceling() {
+        IProgressMonitor mon = fMonitor;
+        if ((mon != null) && (!mon.isCanceled())) {
+            mon.setCanceled(true);
+        }
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/IRosMessageFlowModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/IRosMessageFlowModel.java
new file mode 100644
index 0000000..5fac91f
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/IRosMessageFlowModel.java
@@ -0,0 +1,38 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModel;
+
+/**
+ * Interface that represents the model for ROS message flow, which is a "graph"
+ * of the path a message goes take through ROS nodes, including
+ * publisher/subscriber queues and callbacks
+ *
+ * @author Christophe Bedard
+ */
+public interface IRosMessageFlowModel extends IRosModel {
+
+    /**
+     * @return true if the model is complete, false otherwise
+     */
+    boolean isModelDone();
+
+    /**
+     * TODO eventually support building segments going backwards
+     *
+     * @return get the first segment of the message flow, or {@code null} if it
+     *         has not been generated
+     * @see
+     */
+    @Nullable
+    RosMessageFlowSegment getFirstSegment();
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowModel.java
new file mode 100644
index 0000000..684cfe8
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowModel.java
@@ -0,0 +1,456 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.AbstractRosStateProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo.RosQueueType;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnectionEndpoint;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment.SegmentType;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport.IRosMessagesTransportModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport.RosMessageTransport;
+import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
+import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException;
+import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
+import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
+
+/**
+ * {@link IRosMessageFlowModel} implementation
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowModel implements IRosMessageFlowModel {
+
+    private final @NonNull TargetMessageInfo fInfo;
+    private final @NonNull ITmfStateSystem fQueuesSs;
+    private final @NonNull IRosMessagesTransportModel fMsgsTransportModel;
+
+    private RosMessageFlowSegment fInitialSegment = null;
+    private boolean fIsModelDone = false;
+
+    /**
+     * Constructor
+     *
+     * @param targetInfo
+     *            the target message info
+     * @param queuesStateSystem
+     *            the queues state system
+     * @param msgsTransportModel
+     *            the messages transport model
+     */
+    public RosMessageFlowModel(@NonNull TargetMessageInfo targetInfo, @NonNull ITmfStateSystem queuesStateSystem, @NonNull IRosMessagesTransportModel msgsTransportModel) {
+        fInfo = targetInfo;
+        fQueuesSs = queuesStateSystem;
+        fMsgsTransportModel = msgsTransportModel;
+    }
+
+    @Override
+    public @Nullable RosMessageFlowSegment getFirstSegment() {
+        return fInitialSegment;
+    }
+
+    @Override
+    public boolean isModelDone() {
+        return fIsModelDone;
+    }
+
+    /**
+     * Contains necessary basic information to get/process the next segment.
+     * Serves as a mutable container.
+     */
+    private class NextSegmentInfo {
+
+        /** Type of the next segment */
+        private SegmentType fType = null;
+        /** Relevant quark for the next segment */
+        private Integer fQuark = null;
+        /** Relevant timestamp for the next segment */
+        private Long fTimestamp = null;
+        /** The next segment that was created */
+        private RosMessageFlowSegment fSegment = null;
+        /** The previous segment that was created */
+        private RosMessageFlowSegment fPreviousSegment = null;
+
+        public NextSegmentInfo() {
+        }
+
+        public SegmentType getType() {
+            return fType;
+        }
+
+        public void setType(SegmentType type) {
+            fType = type;
+        }
+
+        public Integer getQuark() {
+            return fQuark;
+        }
+
+        public void setQuark(int quark) {
+            fQuark = quark;
+        }
+
+        public Long getTimestamp() {
+            return fTimestamp;
+        }
+
+        public void setTimestamp(long t) {
+            fTimestamp = t;
+        }
+
+        public RosMessageFlowSegment getNextSegment() {
+            return fSegment;
+        }
+
+        public void setNextSegment(RosMessageFlowSegment segment) {
+            fPreviousSegment = fSegment;
+            fSegment = segment;
+        }
+
+        public RosMessageFlowSegment getPreviousSegment() {
+            return fPreviousSegment;
+        }
+    }
+
+    @Override
+    public void generateModel() {
+        try {
+            process();
+            fIsModelDone = true;
+        } catch (AttributeNotFoundException | StateSystemDisposedException e) {
+            Activator.getInstance().logError("Error while processing! ", e); //$NON-NLS-1$
+        }
+    }
+
+    private void process() throws AttributeNotFoundException, StateSystemDisposedException {
+        // Find type of initial segment corresponding to target message
+        SegmentType type = getSegmentTypeFromQueueType(fInfo.getQueueType());
+        // Find other necessary info about first segment
+        int queueQuark = getTargetQueueQuark();
+        int queuePos = fInfo.getQueuePosition();
+        int queuePosQuark = fQueuesSs.getQuarkRelative(queueQuark, String.valueOf(queuePos));
+        NextSegmentInfo next = new NextSegmentInfo();
+        next.setType(type);
+        next.setQuark(queuePosQuark);
+        next.setTimestamp(fInfo.getMsgTimestamp());
+
+        while (next.getType() != SegmentType.INVALID) {
+            // process
+            switch (next.getType()) {
+            case PUB_QUEUE:
+                processPubQueue(next);
+                break;
+            case SUB_QUEUE:
+                processSubQueue(next);
+                break;
+            case SUB_CALLBACK:
+                processCallback(next);
+                break;
+            case INVALID:
+            default:
+                Activator.getInstance().logError("Case " + next.getType().name() + " should be handled!"); //$NON-NLS-1$ //$NON-NLS-2$
+                break;
+            }
+
+            // Link previous to current
+            RosMessageFlowSegment nextSegment = next.getNextSegment();
+            RosMessageFlowSegment previousSegment = next.getPreviousSegment();
+            if (previousSegment == null) {
+                fInitialSegment = nextSegment;
+            } else {
+                previousSegment.addNext(nextSegment);
+            }
+        }
+    }
+
+    private void processPubQueue(NextSegmentInfo next) throws StateSystemDisposedException, AttributeNotFoundException {
+        int queuePosQuark = next.getQuark();
+        long msgStart = next.getTimestamp();
+
+        ITmfStateInterval firstState = fQueuesSs.querySingleState(msgStart, queuePosQuark);
+        ITmfStateInterval lastState = getLastStateOfMessageInQueue(firstState, queuePosQuark);
+        int queueQuark = fQueuesSs.getParentAttributeQuark(queuePosQuark);
+
+        long start = firstState.getStartTime();
+        long end = lastState.getEndTime();
+        int topicQuark = fQueuesSs.getParentAttributeQuark(fQueuesSs.getParentAttributeQuark(queuePosQuark));
+        int nodeQuark = fQueuesSs.getParentAttributeQuark(fQueuesSs.getParentAttributeQuark(topicQuark));
+        String nodeName = fQueuesSs.getAttributeName(nodeQuark);
+        String topicName = fQueuesSs.getAttributeName(topicQuark);
+        RosMessageFlowSegment segment = new RosMessageFlowSegment(start, end, next.getType(), nodeName, topicName);
+        next.setNextSegment(segment);
+
+        QueueSegmentTransition transition = getTransitionFromLastQueueState(lastState, fInfo.getQueueType(), queueQuark);
+        if (transition == null) {
+            Activator.getInstance().logError("Could not find transition!"); //$NON-NLS-1$
+        }
+
+        if (transition == null || transition == QueueSegmentTransition.DROP) {
+            next.setType(SegmentType.INVALID);
+        } else {
+            next.setType(SegmentType.SUB_QUEUE);
+            // Find first state after network
+            RosMessageTransport transport = fMsgsTransportModel.getNextMessageTransport(end, nodeName, topicName);
+            if (transport == null) {
+                Activator.getInstance().logError("Could not find next message transport!"); //$NON-NLS-1$
+                next.setType(SegmentType.INVALID);
+            } else {
+                // Find the subscriber queue from connection
+                RosConnectionEndpoint sub = transport.getConnection().getSub();
+                queueQuark = fQueuesSs.getQuarkAbsolute(sub.getNodeName(), AbstractRosStateProvider.SUBSCRIBERS_LIST, sub.getTopicName(), AbstractRosStateProvider.QUEUE);
+
+                ITmfStateInterval stateBeforeMsg = getStateBeforeMessageAddedToQueue(queueQuark, transport.getDestinationTimestamp());
+                int incomingMsgPosQuark = stateBeforeMsg.getAttribute();
+                long msgStateStartTime = stateBeforeMsg.getEndTime() + 1;
+
+                next.setQuark(incomingMsgPosQuark);
+                next.setTimestamp(msgStateStartTime);
+            }
+        }
+    }
+
+    private void processSubQueue(NextSegmentInfo next) throws AttributeNotFoundException, StateSystemDisposedException {
+        int incomingMsgPosQuark = next.getQuark();
+        long msgStateStartTime = next.getTimestamp();
+        ITmfStateInterval firstState = fQueuesSs.querySingleState(msgStateStartTime, incomingMsgPosQuark);
+        ITmfStateInterval lastState = getLastStateOfMessageInQueue(firstState, incomingMsgPosQuark);
+
+        long start = firstState.getStartTime();
+        long end = lastState.getEndTime();
+        int topicQuark = fQueuesSs.getParentAttributeQuark(fQueuesSs.getParentAttributeQuark(incomingMsgPosQuark));
+        int nodeQuark = fQueuesSs.getParentAttributeQuark(fQueuesSs.getParentAttributeQuark(topicQuark));
+        String nodeName = fQueuesSs.getAttributeName(nodeQuark);
+        String topicName = fQueuesSs.getAttributeName(topicQuark);
+        RosMessageFlowSegment segment = new RosMessageFlowSegment(start, end, next.getType(), nodeName, topicName);
+        next.setNextSegment(segment);
+
+        QueueSegmentTransition transition = getTransitionFromLastQueueState(lastState, RosQueueType.SUB, fQueuesSs.getParentAttributeQuark(incomingMsgPosQuark));
+        if (transition == null) {
+            Activator.getInstance().logError("Could not find transition!"); //$NON-NLS-1$
+        }
+
+        if (transition == null || transition == QueueSegmentTransition.DROP) {
+            next.setType(SegmentType.INVALID);
+        } else {
+            next.setType(SegmentType.SUB_CALLBACK);
+
+            // Callback state info
+            long callbackStart = lastState.getEndTime() + 1;
+            // Callbacks are not processed per-topic, but per-node/globally
+            int subsQuark = fQueuesSs.getParentAttributeQuark(topicQuark);
+            int callbackQuark = fQueuesSs.getQuarkRelative(subsQuark, AbstractRosStateProvider.CALLBACKS);
+
+            next.setQuark(callbackQuark);
+            next.setTimestamp(callbackStart);
+        }
+    }
+
+    private void processCallback(NextSegmentInfo next) throws StateSystemDisposedException {
+        long callbackStart = next.getTimestamp();
+        int callbackQuark = next.getQuark();
+        ITmfStateInterval callbackState = fQueuesSs.querySingleState(callbackStart, callbackQuark);
+        int nodeQuark = fQueuesSs.getParentAttributeQuark(fQueuesSs.getParentAttributeQuark(callbackQuark));
+
+        // Find messages added to the pub queues during the callback
+        List<ITmfStateInterval> pubMsgs = new ArrayList<>();
+        try {
+            int pubListQuark = fQueuesSs.getQuarkRelative(nodeQuark, AbstractRosStateProvider.PUBLISHERS_LIST);
+            List<@NonNull Integer> pubTopicQuarks = fQueuesSs.getSubAttributes(pubListQuark, false);
+            for (Integer pubTopicQuark : pubTopicQuarks) {
+                String topicName = fQueuesSs.getAttributeName(pubTopicQuark);
+                // Rejecting rosout for now
+                // TODO put back in and support
+                if (!topicName.contains("rosout")) { //$NON-NLS-1$
+                    int queueQuark = fQueuesSs.getQuarkRelative(pubTopicQuark, AbstractRosStateProvider.QUEUE);
+                    ITmfStateInterval stateBefore = getStateBeforeMessageAddedToQueue(queueQuark, callbackState.getStartTime() - 1);
+                    // If there is a new state within the start/end of the
+                    // callback
+                    if (stateBefore.getEndTime() < callbackState.getEndTime()) {
+                        long newMsgsStart = stateBefore.getEndTime() + 1;
+                        ITmfStateInterval newPubMsg = fQueuesSs.querySingleState(newMsgsStart, stateBefore.getAttribute());
+                        pubMsgs.add(newPubMsg);
+                    }
+                }
+            }
+        } catch (AttributeNotFoundException e) {
+            // Do nothing, assume it means there are no more messages to be
+            // found
+        }
+
+        long start = callbackState.getStartTime();
+        long end;
+        String nodeName = fQueuesSs.getAttributeName(nodeQuark);
+        // No specific topic associated with a callback
+        String topicName = StringUtils.EMPTY;
+
+        if (pubMsgs.isEmpty()) {
+            // No messages found
+            // Still add a segment for time spent in callback
+            end = callbackState.getEndTime();
+            RosMessageFlowSegment segment = new RosMessageFlowSegment(start, end, next.getType(), nodeName, topicName);
+            next.setNextSegment(segment);
+            next.setType(SegmentType.INVALID);
+        } else {
+            // We do not support non-linear flow graphs, so just warn for now
+            // and take the first message
+            // TODO support more than one published message
+            if (pubMsgs.size() > 1) {
+                Activator.getInstance().logWarning("Found more than one published message during callback!"); //$NON-NLS-1$
+            }
+
+            ITmfStateInterval pubMsg = pubMsgs.get(0);
+            end = pubMsg.getStartTime() - 1;
+            RosMessageFlowSegment segment = new RosMessageFlowSegment(start, end, next.getType(), nodeName, topicName);
+            next.setNextSegment(segment);
+
+            next.setQuark(pubMsg.getAttribute());
+            next.setTimestamp(pubMsg.getStartTime());
+            next.setType(SegmentType.PUB_QUEUE);
+        }
+    }
+
+    /**
+     * Get the state before a new message is (possibly) added to a queue after a
+     * specific timestamp
+     *
+     * @param queueQuark
+     *            the quark of the queue to consider
+     * @param initialTimestamp
+     *            the last timestamp before the new message could be added to
+     *            the queue (e.g. 1 ns before)
+     * @return the state before a new message is (possibly) added to the queue
+     * @throws StateSystemDisposedException
+     * @throws AttributeNotFoundException
+     */
+    private ITmfStateInterval getStateBeforeMessageAddedToQueue(int queueQuark, long initialTimestamp) throws StateSystemDisposedException, AttributeNotFoundException {
+        // Get the size of the queue right when the message
+        // is about to be added to it
+        ITmfStateInterval queueState = fQueuesSs.querySingleState(initialTimestamp, queueQuark);
+        int queueSize = (queueState.getValue() == null) ? 0 : queueState.getValueInt();
+        // Get the state at that moment; the state of the new message added to
+        // the queue should be right after, with the same quark/queue position
+        int incomingMsgPos = queueSize + 1;
+        int incomingMsgPosQuark = fQueuesSs.getQuarkRelative(queueQuark, String.valueOf(incomingMsgPos));
+        ITmfStateInterval stateBeforeNewMsg = fQueuesSs.querySingleState(initialTimestamp, incomingMsgPosQuark);
+        return stateBeforeNewMsg;
+    }
+
+    private static SegmentType getSegmentTypeFromQueueType(RosQueueType queueType) {
+        switch (queueType) {
+        case SUB:
+            return SegmentType.SUB_QUEUE;
+        case PUB:
+            return SegmentType.PUB_QUEUE;
+        default:
+            Activator.getInstance().logError("Case " + queueType.name() + " should be handled!"); //$NON-NLS-1$ //$NON-NLS-2$
+            return null;
+        }
+    }
+
+    /**
+     * Get the message's next transition after a queue
+     *
+     * @param lastState
+     *            the last state of the message in the queue (at position 1)
+     * @return the next transition, or {@code null} if unknown
+     * @throws AttributeNotFoundException
+     * @throws StateSystemDisposedException
+     */
+    private QueueSegmentTransition getTransitionFromLastQueueState(ITmfStateInterval lastState, RosQueueType queueType, int queueQuark) throws StateSystemDisposedException {
+        // TODO check for "dropped" latched messages in pub queues!
+
+        // Check for drop
+        int topicQuark = fQueuesSs.getParentAttributeQuark(queueQuark);
+        try {
+            int dropsQuark = fQueuesSs.getQuarkRelative(topicQuark, AbstractRosStateProvider.DROPS);
+            ITmfStateInterval possibleDrop = fQueuesSs.querySingleState(lastState.getStartTime(), dropsQuark);
+            if (possibleDrop.getValue() != null) {
+                return QueueSegmentTransition.DROP;
+            }
+        } catch (AttributeNotFoundException e) {
+            // Do nothing, we know there's no drop if there is no "drops" quark
+        }
+
+        switch (queueType) {
+        case PUB:
+            return QueueSegmentTransition.NETWORK;
+        case SUB:
+            return QueueSegmentTransition.CALLBACK;
+        default:
+            Activator.getInstance().logError("Case " + queueType.name() + " should be handled!"); //$NON-NLS-1$ //$NON-NLS-2$
+            return null;
+        }
+    }
+
+    /**
+     * Get the last state of a given message in a queue
+     *
+     * @param message
+     *            one of the states of the message
+     * @param queuePosQuark
+     *            the quark corresponding to the message state
+     * @return the last state of this message in the queue
+     * @throws AttributeNotFoundException
+     * @throws StateSystemDisposedException
+     */
+    private ITmfStateInterval getLastStateOfMessageInQueue(ITmfStateInterval message, int queuePosQuark) throws AttributeNotFoundException, StateSystemDisposedException {
+        ITmfStateInterval msg = message;
+        int queueQuark = fQueuesSs.getParentAttributeQuark(queuePosQuark);
+        int pos = Integer.parseInt(fQueuesSs.getAttributeName(queuePosQuark));
+        long ref = msg.getValueLong();
+        while (pos > 1) {
+            pos--;
+            long nextStartTime = msg.getEndTime() + 1;
+            int nextQueuePosQuark = fQueuesSs.getQuarkRelative(queueQuark, String.valueOf(pos));
+            msg = fQueuesSs.querySingleState(nextStartTime, nextQueuePosQuark);
+            // Check continuity
+            if (msg.getValueLong() != ref) {
+                Activator.getInstance().logWarning("References do not match! previous=" + ref + " vs. now=" + msg.getValueLong()); //$NON-NLS-1$ //$NON-NLS-2$
+            }
+            ref = msg.getValueLong();
+        }
+        return msg;
+    }
+
+    private int getTargetQueueQuark() throws AttributeNotFoundException {
+        return fQueuesSs.getQuarkAbsolute(fInfo.getNode(), getQueueTypeName(fInfo.getQueueType()), fInfo.getTopic(), AbstractRosStateProvider.QUEUE);
+    }
+
+    /**
+     * Possible transitions for a message after waiting in a queue
+     */
+    private enum QueueSegmentTransition {
+        /** Transition to a callback */
+        CALLBACK,
+        /** Sent through the network (message write) */
+        NETWORK,
+        /** Dropped from the queue */
+        DROP
+    }
+
+    private static @Nullable String getQueueTypeName(RosQueueType type) {
+        switch (type) {
+        case PUB:
+            return AbstractRosStateProvider.PUBLISHERS_LIST;
+        case SUB:
+            return AbstractRosStateProvider.SUBSCRIBERS_LIST;
+        default:
+            Activator.getInstance().logError("Case " + type.name() + " should be handled!"); //$NON-NLS-1$ //$NON-NLS-2$
+            return null;
+        }
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowSegment.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowSegment.java
new file mode 100644
index 0000000..b86af16
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messageflow/RosMessageFlowSegment.java
@@ -0,0 +1,194 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Represents a single message flow segment, with a parent/children (or not), a
+ * type, and some other information
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowSegment {
+
+    private final long fStartTime;
+    private final long fEndTime;
+    private final SegmentType fType;
+    private final String fNodeName;
+    private final String fTopicName;
+
+    private @Nullable RosMessageFlowSegment fPrevious = null;
+    private final Collection<RosMessageFlowSegment> fNext = new ArrayList<>();
+
+    /**
+     * {@link RosMessageFlowSegment} type
+     *
+     * @author Christophe Bedard
+     */
+    public enum SegmentType {
+        /** Subscriber queue */
+        SUB_QUEUE,
+        /** Subscriber callback */
+        SUB_CALLBACK,
+        /** Publisher queue */
+        PUB_QUEUE,
+        /** Invalid/nothing */
+        INVALID
+    }
+
+    /**
+     * Constructor
+     *
+     * @param startTime
+     *            the start timestamp of the segment
+     * @param endTime
+     *            the end timestamp of the segment
+     * @param type
+     *            the segment type
+     * @param nodeName
+     *            the name of the node associated with the segment
+     * @param topicName
+     *            the topic associated with the segment
+     */
+    public RosMessageFlowSegment(long startTime, long endTime, SegmentType type, String nodeName, String topicName) {
+        this(startTime, endTime, type, nodeName, topicName, null);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param start
+     *            the start timestamp of the segment
+     * @param end
+     *            the end timestamp of the segment
+     * @param type
+     *            the segment type
+     * @param nodeName
+     *            the name of the node associated with the segment
+     * @param topicName
+     *            the topic associated with the segment
+     * @param previous
+     *            the previous segment
+     */
+    public RosMessageFlowSegment(long start, long end, SegmentType type, String nodeName, String topicName, RosMessageFlowSegment previous) {
+        fStartTime = start;
+        fEndTime = end;
+        fType = type;
+        fNodeName = nodeName;
+        fTopicName = topicName;
+        fPrevious = previous;
+    }
+
+    /**
+     * @param previous
+     *            the previous segment
+     */
+    public void setPrevious(RosMessageFlowSegment previous) {
+        fPrevious = previous;
+    }
+
+    /**
+     * @param next
+     *            the segment to add to the collection of next segments
+     */
+    public void addNext(RosMessageFlowSegment next) {
+        fNext.add(next);
+    }
+
+    /**
+     * @return the previous segment, or {@code null} if this is the first
+     *         segment
+     */
+    public @Nullable RosMessageFlowSegment getPrevious() {
+        return fPrevious;
+    }
+
+    /**
+     * @return true if there is a previous segment, false otherwise
+     */
+    public boolean hasPrevious() {
+        return fPrevious != null;
+    }
+
+    /**
+     * @return the next segment(s), or empty if it's the last one
+     */
+    public Collection<RosMessageFlowSegment> getNext() {
+        return fNext;
+    }
+
+    /**
+     * @return true if there is a next segment, false otherwise
+     */
+    public boolean hasNext() {
+        return !fNext.isEmpty();
+    }
+
+    /**
+     * @return the start timestamp of the segment
+     */
+    public long getStartTime() {
+        return fStartTime;
+    }
+
+    /**
+     * @return the end timestamp of the segment
+     */
+    public long getEndTime() {
+        return fEndTime;
+    }
+
+    /**
+     * @return the segment type
+     */
+    public SegmentType getType() {
+        return fType;
+    }
+
+    /**
+     * @return the name of the node associated with the segment
+     */
+    public String getNodeName() {
+        return fNodeName;
+    }
+
+    /**
+     * @return the topic associated with the segment
+     */
+    public String getTopicName() {
+        return fTopicName;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fEndTime, fNext, fNodeName, fPrevious, fStartTime, fTopicName, fType);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof RosMessageFlowSegment)) {
+            return false;
+        }
+        RosMessageFlowSegment other = (RosMessageFlowSegment) obj;
+        return fEndTime == other.fEndTime && Objects.equals(fNext, other.fNext) && Objects.equals(fNodeName, other.fNodeName) && Objects.equals(fPrevious, other.fPrevious) && fStartTime == other.fStartTime
+                && Objects.equals(fTopicName, other.fTopicName) && fType == other.fType;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/IRosMessagesTransportModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/IRosMessagesTransportModel.java
new file mode 100644
index 0000000..9cac7c0
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/IRosMessagesTransportModel.java
@@ -0,0 +1,47 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModel;
+
+/**
+ * Interface that represents the model for ROS messages transport, with info on
+ * message exchanges between nodes
+ *
+ * @author Christophe Bedard
+ */
+public interface IRosMessagesTransportModel extends IRosModel {
+
+    /**
+     * Get the next message transport corresponding to a node publishing on a
+     * topic after a timestamp
+     *
+     * @param timestamp
+     *            the timestamp after which to find the transport
+     * @param node
+     *            the node name (publisher)
+     * @param topicName
+     *            the topic name
+     * @return the next message transport, or {@code null} if it doesn't exist
+     */
+    @Nullable
+    RosMessageTransport getNextMessageTransport(long timestamp, String node, String topicName);
+
+    /**
+     * @return an unmodifiable view of the {@link RosMessageTransport} instances
+     *         the model has, or {@code null} if the model has not been
+     *         generated yet
+     */
+    @Nullable
+    Collection<RosMessageTransport> getMessageTransports();
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/NetworkConnection.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/NetworkConnection.java
new file mode 100644
index 0000000..76f3682
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/NetworkConnection.java
@@ -0,0 +1,74 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.Objects;
+
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.ConnectionEndpoint;
+
+/**
+ * Represents a network connection, with source and destination
+ * {@link ConnectionEndpoint}s
+ *
+ * @author Christophe Bedard
+ */
+public class NetworkConnection {
+
+    private final ConnectionEndpoint fSourceEndpoint;
+    private final ConnectionEndpoint fDestinationEndpoint;
+
+    /**
+     * Constructor
+     *
+     * @param source
+     *            the source endpoint
+     * @param destination
+     *            the destination endpoint
+     */
+    public NetworkConnection(ConnectionEndpoint source, ConnectionEndpoint destination) {
+        fSourceEndpoint = source;
+        fDestinationEndpoint = destination;
+    }
+
+    /**
+     * @return the source endpoint
+     */
+    public ConnectionEndpoint getSourceEndpoint() {
+        return fSourceEndpoint;
+    }
+
+    /**
+     * @return the destination endpoint
+     */
+    public ConnectionEndpoint getDestinationEndpoint() {
+        return fDestinationEndpoint;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(fDestinationEndpoint, fSourceEndpoint);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof NetworkConnection)) {
+            return false;
+        }
+        NetworkConnection other = (NetworkConnection) obj;
+        return Objects.equals(fDestinationEndpoint, other.fDestinationEndpoint)
+                && Objects.equals(fSourceEndpoint, other.fSourceEndpoint);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessageTransport.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessageTransport.java
new file mode 100644
index 0000000..d60236f
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessageTransport.java
@@ -0,0 +1,84 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnection;
+
+/**
+ * Contains information about a specific instance of a message that was
+ * transported between two nodes
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageTransport {
+
+    private final RosConnection fConnection;
+    private final long fSourceTimestamp;
+    private final long fDestinationTimestamp;
+    // TODO remove, debug
+    private final RosMessagesTransportEventKey fKey;
+
+    /**
+     * Constructor
+     *
+     * @param connection
+     *            the {@link RosConnection} responsible for this message
+     *            transport
+     * @param sourceTimestamp
+     *            the timestamp of the network transport source event (e.g.
+     *            net_dev_queue)
+     * @param destinationTimestamp
+     *            the timestamp of the network transport destination event (e.g.
+     *            netif_receive_skb)
+     */
+    public RosMessageTransport(RosConnection connection, long sourceTimestamp, long destinationTimestamp, RosMessagesTransportEventKey key) {
+        fConnection = connection;
+        fSourceTimestamp = sourceTimestamp;
+        fDestinationTimestamp = destinationTimestamp;
+        fKey = key;
+    }
+
+    /**
+     * @return the {@link RosConnection} responsible for this message transport
+     */
+    public RosConnection getConnection() {
+        return fConnection;
+    }
+
+    /**
+     * @return the timestamp of the network transport source (e.g.
+     *         net_dev_queue)
+     */
+    public long getSourceTimestamp() {
+        return fSourceTimestamp;
+    }
+
+    /**
+     * @return the timestamp of the network transport destination (e.g.
+     *         netif_receive_skb)
+     */
+    public long getDestinationTimestamp() {
+        return fDestinationTimestamp;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("RosMessageTransport"); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\t" + fConnection.toString()); //$NON-NLS-1$
+        b.append("\tSource     : " + String.valueOf(fSourceTimestamp)); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\tDestination: " + String.valueOf(fDestinationTimestamp)); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        b.append("\tkey: " + fKey.toString()); //$NON-NLS-1$
+        return b.toString();
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventKey.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventKey.java
new file mode 100644
index 0000000..49049ea
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventKey.java
@@ -0,0 +1,89 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.tmf.core.event.matching.TcpEventKey;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+/**
+ * Event matching key based on {@link TcpEventKey} with additional information
+ * on the connection information
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessagesTransportEventKey extends TcpEventKey {
+
+    private static final HashFunction HF = checkNotNull(Hashing.goodFastHash(32));
+
+    private final NetworkConnection fConnectionInfo;
+    private final long fFlags;
+
+    /**
+     * Constructor
+     *
+     * @param sequence
+     *            The sequence number of the TCP packet
+     * @param ack
+     *            The acknowledgement number of the TCP packet
+     * @param flags
+     *            The 16 bits following the acknowledgment: data offset,
+     *            reserved and flags)
+     * @param connectionInfo
+     *            the network connection information
+     */
+    public RosMessagesTransportEventKey(long sequence, long ack, long flags, NetworkConnection connectionInfo) {
+        super(sequence, ack, flags);
+        fConnectionInfo = connectionInfo;
+        // Needed for match confirmation
+        fFlags = flags;
+    }
+
+    /**
+     * @return the flags associated with this packet event
+     */
+    public long getFlags() {
+        return fFlags;
+    }
+
+    /**
+     * @return the network connection information
+     */
+    public NetworkConnection getNetworkConnectionInfo() {
+        return fConnectionInfo;
+    }
+
+    @Override
+    public int hashCode() {
+        return HF.newHasher().putInt(super.hashCode()).putInt(fConnectionInfo.hashCode()).hash().asInt();
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (o instanceof RosMessagesTransportEventKey) {
+            RosMessagesTransportEventKey key = (RosMessagesTransportEventKey) o;
+            return super.equals(o) && key.fConnectionInfo.equals(fConnectionInfo);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder b = new StringBuilder();
+        b.append("RosMessagesTransportEventKey:\n"); //$NON-NLS-1$
+        b.append("\t\tflags: " + fFlags); //$NON-NLS-1$
+        b.append("\n"); //$NON-NLS-1$
+        return b.toString();
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatching.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatching.java
new file mode 100644
index 0000000..a04000f
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatching.java
@@ -0,0 +1,109 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.tracecompass.analysis.os.linux.core.trace.IKernelAnalysisEventLayout;
+import org.eclipse.tracecompass.analysis.os.linux.core.trace.IKernelTrace;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.ConnectionEndpoint;
+import org.eclipse.tracecompass.internal.lttng2.kernel.core.TcpEventStrings;
+import org.eclipse.tracecompass.internal.lttng2.kernel.core.event.matching.TcpLttngEventMatching;
+import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
+import org.eclipse.tracecompass.tmf.core.event.TmfEventField;
+import org.eclipse.tracecompass.tmf.core.event.matching.IEventMatchingKey;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTraceWithPreDefinedEvents;
+import org.eclipse.tracecompass.tmf.core.trace.TmfEventTypeCollectionHelper;
+import org.eclipse.tracecompass.tmf.ctf.core.event.CtfTmfEventField;
+
+/**
+ * Class to match TCP events with useful ROS connection context
+ *
+ * FIXME code duplication with TcpLttngEventMatching
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessagesTransportEventMatching extends TcpLttngEventMatching {
+
+    // FIXME get base path from layout instead
+    private static final @NonNull String[] FIELD_PATH_TO_SADDR = { "network_header", CtfTmfEventField.FIELD_VARIANT_SELECTED, TcpEventStrings.SADDR }; //$NON-NLS-1$
+    private static final @NonNull String[] FIELD_PATH_TO_DADDR = { "network_header", CtfTmfEventField.FIELD_VARIANT_SELECTED, TcpEventStrings.DADDR }; //$NON-NLS-1$
+    private static final @NonNull String[] FIELD_PATH_TO_SOURCE_PORT = { "network_header", CtfTmfEventField.FIELD_VARIANT_SELECTED, "transport_header", "tcp", "source_port" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+    private static final @NonNull String[] FIELD_PATH_TO_DEST_PORT = { "network_header", CtfTmfEventField.FIELD_VARIANT_SELECTED, "transport_header", "tcp", "dest_port" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+
+    private static final Map<IKernelAnalysisEventLayout, Set<String>> REQUIRED_EVENTS = new HashMap<>();
+
+    /** Use a weak hash map so that traces can be garbage collected */
+    private static final Map<ITmfTrace, IKernelAnalysisEventLayout> TRACE_LAYOUTS = new WeakHashMap<>();
+
+    @Override
+    public boolean canMatchTrace(ITmfTrace trace) {
+        // Get the events that this trace needs to have
+        if (!(trace instanceof IKernelTrace)) {
+            // Not a kernel trace, we cannot know what events to use, return
+            // false
+            return false;
+        }
+        IKernelAnalysisEventLayout layout = ((IKernelTrace) trace).getKernelEventLayout();
+        TRACE_LAYOUTS.put(trace, layout);
+
+        Set<String> events = REQUIRED_EVENTS.get(layout);
+        if (events == null) {
+            events = new HashSet<>();
+            events.addAll(layout.eventsNetworkSend());
+            events.addAll(layout.eventsNetworkReceive());
+            REQUIRED_EVENTS.put(layout, events);
+        }
+
+        if (!(trace instanceof ITmfTraceWithPreDefinedEvents)) {
+            // No predefined events, suppose events are present
+            return true;
+        }
+        ITmfTraceWithPreDefinedEvents ktrace = (ITmfTraceWithPreDefinedEvents) trace;
+
+        Set<String> traceEvents = TmfEventTypeCollectionHelper.getEventNames(ktrace.getContainedEventTypes());
+        traceEvents.retainAll(events);
+        return !traceEvents.isEmpty();
+    }
+
+    @Override
+    public IEventMatchingKey getEventKey(ITmfEvent event) {
+        IKernelAnalysisEventLayout layout = TRACE_LAYOUTS.get(event.getTrace());
+        if (layout == null) {
+            return null;
+        }
+
+        TmfEventField content = (TmfEventField) event.getContent();
+
+        Long sequence = content.getFieldValue(Long.class, layout.fieldPathTcpSeq());
+        Long ack = content.getFieldValue(Long.class, layout.fieldPathTcpAckSeq());
+        Long flags = content.getFieldValue(Long.class, layout.fieldPathTcpFlags());
+
+        long[] saddr = content.getFieldValue(long[].class, FIELD_PATH_TO_SADDR);
+        Long sport = content.getFieldValue(Long.class, FIELD_PATH_TO_SOURCE_PORT);
+        long[] daddr = content.getFieldValue(long[].class, FIELD_PATH_TO_DADDR);
+        Long dport = content.getFieldValue(Long.class, FIELD_PATH_TO_DEST_PORT);
+
+        if (sequence == null || ack == null || flags == null || saddr == null || sport == null || daddr == null || dport == null) {
+            return null;
+        }
+
+        ConnectionEndpoint sourceEndpoint = new ConnectionEndpoint(saddr, sport);
+        ConnectionEndpoint destinationEndpoint = new ConnectionEndpoint(daddr, dport);
+        NetworkConnection connection = new NetworkConnection(sourceEndpoint, destinationEndpoint);
+        return new RosMessagesTransportEventKey(sequence, ack, flags, connection);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatchingProcessing.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatchingProcessing.java
new file mode 100644
index 0000000..98e15b4
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportEventMatchingProcessing.java
@@ -0,0 +1,123 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.IRosConnectionsModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnection;
+import org.eclipse.tracecompass.tmf.core.event.matching.IEventMatchingKey;
+import org.eclipse.tracecompass.tmf.core.event.matching.IMatchProcessingUnit;
+import org.eclipse.tracecompass.tmf.core.event.matching.TmfEventDependency;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+
+/**
+ * Match processing unit for ROS messages transport. Validates matches according
+ * to a {@link IRosConnectionsModel} and processes the information to eventually
+ * provide it to the {@link RosMessagesTransportModel}
+ *
+ * FIXME figure out why it sometimes does not work
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessagesTransportEventMatchingProcessing implements IMatchProcessingUnit {
+
+    private final @NonNull IRosConnectionsModel fConnectionsModel;
+    private int fCount = 0;
+    private final Collection<RosMessageTransport> fMatches = new ArrayList<>();
+
+    /**
+     * Constructor
+     *
+     * @param connectionsModel
+     *            the connections model, to validate match
+     */
+    public RosMessagesTransportEventMatchingProcessing(@NonNull IRosConnectionsModel connectionsModel) {
+        fConnectionsModel = connectionsModel;
+    }
+
+    /**
+     * @return the matches
+     */
+    public Collection<RosMessageTransport> getMatches() {
+        return fMatches;
+    }
+
+    @Override
+    public void init(@NonNull Collection<@NonNull ITmfTrace> fTraces) {
+        // Nothing to do
+    }
+
+    @Override
+    public void addMatch(@NonNull TmfEventDependency match) {
+        // Not used, since we need the event key
+        Activator.getInstance().logError("Event key is needed to process a match!"); //$NON-NLS-1$
+    }
+
+    @Override
+    public void addMatch(@NonNull IEventMatchingKey eventKey, @NonNull TmfEventDependency match) {
+        if (!(eventKey instanceof RosMessagesTransportEventKey)) {
+            return;
+        }
+
+        RosMessagesTransportEventKey key = (RosMessagesTransportEventKey) eventKey;
+        // Might not be an actual ROS message
+        if (!isValidMessageMatch(key)) {
+            return;
+        }
+
+        // Check that the connection is in the RosConnectionsModel; ignore other
+        // network traffic
+        @Nullable RosConnection rosConnection = getRosConnection(key);
+        if (rosConnection == null) {
+            return;
+        }
+
+        // Process and add to the matches with important info
+        fMatches.add(new RosMessageTransport(
+                rosConnection,
+                match.getSource().getTimestamp().toNanos(),
+                match.getDestination().getTimestamp().toNanos(),
+                key));
+        fCount++;
+    }
+
+    /**
+     * Figure out if the match corresponds to an actual message transmission
+     *
+     * @param key
+     *            the event key
+     * @return true if it corresponds to a message, false otherwise
+     */
+    private static boolean isValidMessageMatch(@NonNull RosMessagesTransportEventKey key) {
+        // Filter out SYN (2) and FIN (1)
+        return !((key.getFlags() & 2) != 0 || (key.getFlags() & 1) != 0);
+    }
+
+    @Override
+    public void matchingEnded() {
+        // Nothing to do
+    }
+
+    @Override
+    public int countMatches() {
+        return fCount;
+    }
+
+    private @Nullable RosConnection getRosConnection(RosMessagesTransportEventKey key) {
+        return fConnectionsModel.getConnectionBetweenEndpoints(
+                key.getNetworkConnectionInfo().getSourceEndpoint(),
+                key.getNetworkConnectionInfo().getDestinationEndpoint());
+    }
+}
\ No newline at end of file
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModel.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModel.java
new file mode 100644
index 0000000..b8739f7
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModel.java
@@ -0,0 +1,104 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.IRosConnectionsModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnection;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnectionsModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.IRosMessageFlowModel;
+import org.eclipse.tracecompass.tmf.core.event.matching.TmfEventMatching;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+
+/**
+ * {@link IRosMessageFlowModel} implementation. Uses network event matching
+ * along with a {@link RosConnectionsModel}.
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessagesTransportModel implements IRosMessagesTransportModel {
+
+    private static RosMessagesTransportEventMatching sfMatching = null;
+
+    private final @NonNull IRosConnectionsModel fConnectionsModel;
+    private final RosMessagesTransportEventMatchingProcessing fProcessingUnit;
+    private final TmfEventMatching fMatching;
+    private Collection<RosMessageTransport> fMessageTransports = null;
+
+    /**
+     * Constructor
+     *
+     * @param trace
+     *            the trace
+     * @param connectionsModel
+     *            the connections model
+     */
+    public RosMessagesTransportModel(ITmfTrace trace, @NonNull IRosConnectionsModel connectionsModel) {
+        fConnectionsModel = connectionsModel;
+        fProcessingUnit = new RosMessagesTransportEventMatchingProcessing(fConnectionsModel);
+        fMatching = new TmfEventMatching(Collections.singleton(Objects.requireNonNull(trace)), fProcessingUnit);
+    }
+
+    @Override
+    public void generateModel() {
+        // TODO move to somewhere more appropriate?
+        if (sfMatching == null) {
+            sfMatching = new RosMessagesTransportEventMatching();
+            TmfEventMatching.registerMatchObject(sfMatching);
+        }
+
+        fMatching.matchEvents();
+        fMessageTransports = fProcessingUnit.getMatches();
+    }
+
+    @Override
+    public @Nullable RosMessageTransport getNextMessageTransport(long timestamp, String node, String topicName) {
+        if (fMessageTransports == null) {
+            return null;
+        }
+
+        // Get connection
+        RosConnection connection = fConnectionsModel.getConnectionFromNodePublishingOnTopic(node, topicName);
+        if (connection == null) {
+            Activator.getInstance().logError("Could not find connection!"); //$NON-NLS-1$
+            return null;
+        }
+
+        // Get closest (timewise) transport for that connection
+        long matchDiff = Long.MAX_VALUE;
+        RosMessageTransport match = null;
+        for (RosMessageTransport transport : fMessageTransports) {
+            if (transport.getConnection().equals(connection)) {
+                long diff = transport.getSourceTimestamp() - timestamp;
+                if (diff < matchDiff) {
+                    matchDiff = diff;
+                    match = transport;
+                }
+            }
+        }
+
+        if (match == null) {
+            Activator.getInstance().logError("Could not find closest transport!"); //$NON-NLS-1$
+        }
+
+        return match;
+    }
+
+    @Override
+    public @Nullable Collection<RosMessageTransport> getMessageTransports() {
+        return Collections.unmodifiableCollection(fMessageTransports);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModelAnalysis.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModelAnalysis.java
new file mode 100644
index 0000000..6f35b0f
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/analysis/model/messagestransport/RosMessagesTransportModelAnalysis.java
@@ -0,0 +1,95 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messagestransport;
+
+import java.util.Collections;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.IRosModelProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.RosModelUtils;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.IRosConnectionsModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnectionsModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.connections.RosConnectionsModelAnalysis;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.TmfTraceUtils;
+import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
+
+/**
+ * ROS messages transport model analysis. Creates a
+ * {@link RosMessagesTransportModel} from a {@link RosConnectionsModel} and
+ * networking events.
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessagesTransportModelAnalysis extends TmfAbstractAnalysisModule implements IRosModelProvider<IRosMessagesTransportModel> {
+
+    /** The ID of this analysis module */
+    public static final @NonNull String ID = "org.eclipse.tracecompass.incubator.ros.core.analysis.model.messagestransport"; //$NON-NLS-1$
+
+    private @Nullable IProgressMonitor fMonitor;
+    private @Nullable RosMessagesTransportModel fModel = null;
+
+    /**
+     * Constructor
+     */
+    public RosMessagesTransportModelAnalysis() {
+        setId(ID);
+    }
+
+    @Override
+    public @Nullable RosMessagesTransportModel getModel() {
+        return fModel;
+    }
+
+    @Override
+    protected boolean executeAnalysis(@NonNull IProgressMonitor monitor) throws TmfAnalysisException {
+        ITmfTrace trace = getTrace();
+        if (!(trace instanceof TmfExperiment)) {
+            throw new IllegalStateException();
+        }
+        // Connections model
+        @Nullable
+        IRosConnectionsModel connectionsModel = RosModelUtils.getModelFromAnalysis(trace, RosConnectionsModelAnalysis.class, RosConnectionsModelAnalysis.ID);
+        if (connectionsModel == null) {
+            return false;
+        }
+
+        RosMessagesTransportModel model = new RosMessagesTransportModel(trace, connectionsModel);
+        model.generateModel();
+        fModel = model;
+        return !monitor.isCanceled();
+    }
+
+    @Override
+    protected @NonNull Iterable<@NonNull IAnalysisModule> getDependentAnalyses() {
+        ITmfTrace trace = getTrace();
+        if (trace == null) {
+            return Collections.emptySet();
+        }
+        RosConnectionsModelAnalysis connectionsModel = TmfTraceUtils.getAnalysisModuleOfClass(trace, RosConnectionsModelAnalysis.class, RosConnectionsModelAnalysis.ID);
+        if (connectionsModel == null) {
+            return Collections.emptySet();
+        }
+        return Collections.singleton(connectionsModel);
+    }
+
+    @Override
+    protected void canceling() {
+        IProgressMonitor mon = fMonitor;
+        if ((mon != null) && (!mon.isCanceled())) {
+            mon.setCanceled(true);
+        }
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/signals/RosMessageSelectedSignal.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/signals/RosMessageSelectedSignal.java
new file mode 100644
index 0000000..e9fe627
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/signals/RosMessageSelectedSignal.java
@@ -0,0 +1,43 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.signals;
+
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignal;
+
+/**
+ * ROS message selection signal
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageSelectedSignal extends TmfSignal {
+
+    private final TargetMessageInfo fInfo;
+
+    /**
+     * Constructor
+     *
+     * @param source
+     *            the source
+     * @param info
+     *            the target message info
+     */
+    public RosMessageSelectedSignal(Object source, TargetMessageInfo info) {
+        super(source, 0);
+        fInfo = info;
+    }
+
+    /**
+     * @return the target message info
+     */
+    public TargetMessageInfo getInfo() {
+        return fInfo;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/trace/RosExperiment.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/trace/RosExperiment.java
new file mode 100644
index 0000000..579b1ce
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.core/src/org/eclipse/tracecompass/incubator/internal/ros/core/trace/RosExperiment.java
@@ -0,0 +1,45 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.core.trace;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;
+
+/**
+ * Class for ROS experiments, which combine UST ({@link RosTrace}) and kernel
+ * traces
+ *
+ * @author Christophe Bedard
+ */
+public class RosExperiment extends TmfExperiment {
+
+    /**
+     * Default constructor. Needed by the extension point.
+     */
+    public RosExperiment() {
+        this("", Collections.EMPTY_SET); //$NON-NLS-1$
+    }
+
+    /**
+     * Constructor with traces and id
+     *
+     * @param id
+     *            The ID of this experiment
+     * @param traces
+     *            The set of traces that are part of this experiment
+     */
+    public RosExperiment(String id, Set<ITmfTrace> traces) {
+        super(ITmfEvent.class, id, traces.toArray(new ITmfTrace[traces.size()]), TmfExperiment.DEFAULT_INDEX_PAGE_SIZE, null);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/icons/elcl16/follow_message.gif b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/icons/elcl16/follow_message.gif
new file mode 100644
index 0000000..c256460
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/icons/elcl16/follow_message.gif
Binary files differ
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.properties b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.properties
index 1137fd6..362b058 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.properties
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.properties
@@ -17,3 +17,4 @@
 ros.view.tasks = Tasks (incubator)
 ros.view.connections = Connections (incubator)
 ros.view.timers = Timers (incubator)
+ros.view.messageflow = Message Flow (incubator)
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.xml b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.xml
index 2010441..5e713b5 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.xml
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/plugin.xml
@@ -48,6 +48,14 @@
             name="%ros.view.timers"
             restorable="true">
       </view>
+      <view
+            category="org.eclipse.tracecompass.incubator.internal.ros.ui.views.category"
+            class="org.eclipse.tracecompass.incubator.internal.ros.ui.views.messageflow.RosMessageFlowView"
+            icon="icons/obj16/ros_queues.png"
+            id="org.eclipse.tracecompass.incubator.ros.ui.views.messageflow"
+            name="%ros.view.messageflow"
+            restorable="true">
+      </view>
    </extension>
    <extension
          point="org.eclipse.linuxtools.tmf.ui.tracetypeui">
@@ -79,6 +87,19 @@
                class="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.timers.RosTimersAnalysis">
          </analysisModuleClass>
       </output>
+      <output
+            class="org.eclipse.tracecompass.tmf.ui.analysis.TmfAnalysisViewOutput"
+            id="org.eclipse.tracecompass.incubator.ros.ui.views.messageflow">
+         <analysisModuleClass
+               class="org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowAnalysis">
+         </analysisModuleClass>
+      </output>
+      <parameterProvider
+            class="org.eclipse.tracecompass.incubator.internal.ros.ui.messageflow.RosMessageFlowParameterProvider">
+         <analysisId
+               id="org.eclipse.tracecompass.incubator.ros.core.analysis.messageflow">
+         </analysisId>
+      </parameterProvider>
    </extension>
    <extension
          point="org.eclipse.ui.perspectives">
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/HideRosoutAction.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/HideRosoutAction.java
similarity index 98%
rename from tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/HideRosoutAction.java
rename to tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/HideRosoutAction.java
index 2cd9dc0..9cf54d7 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/HideRosoutAction.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/HideRosoutAction.java
@@ -7,7 +7,7 @@
  * http://www.eclipse.org/legal/epl-v10.html
  **********************************************************************/
 
-package org.eclipse.tracecompass.incubator.internal.ros.ui.views;
+package org.eclipse.tracecompass.incubator.internal.ros.ui.actions;
 
 import java.util.Objects;
 
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/Messages.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/Messages.java
new file mode 100644
index 0000000..026bed5
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/Messages.java
@@ -0,0 +1,40 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.actions;
+
+import org.eclipse.osgi.util.NLS;
+
+/**
+ * Messages for ROS actions
+ *
+ * @author Christophe Bedard
+ */
+public class Messages extends NLS {
+
+    private static final String BUNDLE_NAME = "org.eclipse.tracecompass.incubator.internal.ros.ui.actions.messages"; //$NON-NLS-1$
+
+    /** Name text for HideRosoutAction */
+    public static String HideRosoutAction_NameText;
+    /** Tooltip text for HideRosoutAction */
+    public static String HideRosoutAction_ToolTipText;
+
+    /** Name text for FollowMessageAction */
+    public static String FollowMessageAction_NameText;
+    /** Tooltip text for FollowMessageAction */
+    public static String FollowMessageAction_ToolTipText;
+
+    static {
+        // initialize resource bundle
+        NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+    }
+
+    private Messages() {
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/RosFollowMessageAction.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/RosFollowMessageAction.java
new file mode 100644
index 0000000..9ced1bc
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/RosFollowMessageAction.java
@@ -0,0 +1,96 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.actions;
+
+import java.util.Objects;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.action.Action;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.AbstractRosStateProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo.RosQueueType;
+import org.eclipse.tracecompass.incubator.internal.ros.core.signals.RosMessageSelectedSignal;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.views.queues.RosQueuesView;
+import org.eclipse.tracecompass.tmf.ui.views.TmfView;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphViewer;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
+
+/**
+ * Follow message action. Fetches and validates the selected message and
+ * broadcasts a {@link RosMessageSelectedSignal} if valid
+ *
+ * @author Christophe Bedard
+ */
+public class RosFollowMessageAction extends Action {
+
+    private static final @NonNull String IMG_UI = "icons/elcl16/follow_message.gif"; //$NON-NLS-1$
+
+    /** The corresponding view */
+    private final TmfView fView;
+    /** The time graph viewer */
+    private final TimeGraphViewer fTimeGraphViewer;
+
+    /**
+     * Constructor
+     *
+     * @param source
+     *            the source view
+     * @param timeGraphViewer
+     *            the timegraph viewer
+     */
+    public RosFollowMessageAction(TmfView source, TimeGraphViewer timeGraphViewer) {
+        fTimeGraphViewer = timeGraphViewer;
+        fView = source;
+
+        setText(Messages.FollowMessageAction_NameText);
+        setToolTipText(Messages.FollowMessageAction_ToolTipText);
+        setImageDescriptor(Objects.requireNonNull(Activator.getDefault()).getImageDescripterFromPath(IMG_UI));
+    }
+
+    @Override
+    public void run() {
+        long selectionBegin = fTimeGraphViewer.getSelectionBegin();
+        ITimeGraphEntry selection = fTimeGraphViewer.getSelection();
+        if (selection == null) {
+            return;
+        }
+
+        ITimeEvent msgRefEvent = RosQueuesView.getMessageReferenceEvent(selection, selectionBegin);
+        String msgRef = msgRefEvent.getLabel();
+        if (msgRef != null) {
+            RosQueueType queueType = getQueueTypeFromName(selection.getParent().getParent().getParent().getName());
+            if (queueType == null) {
+                System.err.println("Invalid queue type!"); //$NON-NLS-1$
+                return;
+            }
+
+            String node = selection.getParent().getParent().getParent().getParent().getName();
+            String topic = selection.getParent().getParent().getName();
+            int position = Integer.parseInt(selection.getName());
+            long msgBegin = msgRefEvent.getTime();
+            TargetMessageInfo target = new TargetMessageInfo(node, topic, queueType, position, msgRef, selectionBegin, msgBegin);
+            fView.broadcast(new RosMessageSelectedSignal(fView, target));
+            super.run();
+        }
+    }
+
+    private static @Nullable RosQueueType getQueueTypeFromName(String queueTypeName) {
+        if (queueTypeName.equals(AbstractRosStateProvider.PUBLISHERS_LIST)) {
+            return RosQueueType.PUB;
+        } else if (queueTypeName.equals(AbstractRosStateProvider.SUBSCRIBERS_LIST)) {
+            return RosQueueType.SUB;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/messages.properties b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/messages.properties
new file mode 100644
index 0000000..4c358ae
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/actions/messages.properties
@@ -0,0 +1,15 @@
+###############################################################################
+# Copyright (c) 2019 École Polytechnique de Montréal
+#
+# 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
+#
+###############################################################################
+
+HideRosoutAction_NameText=Hide rosout
+HideRosoutAction_ToolTipText=Hide rosout publishers (which relay a node's logs to the rosout node) and the rosout node (which may redirect logs to stdout/stderr and a file)
+
+FollowMessageAction_NameText=Follow message
+FollowMessageAction_ToolTipText=Follow the selected message
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/messageflow/RosMessageFlowParameterProvider.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/messageflow/RosMessageFlowParameterProvider.java
new file mode 100644
index 0000000..2777fcb
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/messageflow/RosMessageFlowParameterProvider.java
@@ -0,0 +1,79 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.messageflow;
+
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowAnalysis;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.TargetMessageInfo;
+import org.eclipse.tracecompass.incubator.internal.ros.core.signals.RosMessageSelectedSignal;
+import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisParamProvider;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+
+/**
+ * Parameter provider for the ROS Message Flow analysis. Contains the target
+ * message info.
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowParameterProvider extends TmfAbstractAnalysisParamProvider {
+
+    private static final String NAME = "ROS Message Flow parameter provider"; //$NON-NLS-1$
+
+    private TargetMessageInfo fInfo = null;
+
+    /**
+     * Constructor
+     */
+    public RosMessageFlowParameterProvider() {
+        super();
+        TmfSignalManager.register(this);
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        TmfSignalManager.deregister(this);
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+
+    @Override
+    public Object getParameter(String name) {
+        if (name.equals(RosMessageFlowAnalysis.TARGET_MESSAGE_INFO_KEY)) {
+            return fInfo;
+        }
+        return null;
+    }
+
+    @Override
+    public boolean appliesToTrace(ITmfTrace trace) {
+        return true;
+    }
+
+    /**
+     * Handle {@link RosMessageSelectedSignal}
+     *
+     * @param signal
+     *            the signal
+     */
+    @TmfSignalHandler
+    public void handleFollowedMessage(RosMessageSelectedSignal signal) {
+        setTargetMessageInfo(signal.getInfo());
+    }
+
+    private void setTargetMessageInfo(TargetMessageInfo info) {
+        fInfo = info;
+        notifyParameterChanged(RosMessageFlowAnalysis.TARGET_MESSAGE_INFO_KEY);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/AbstractRosView.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/AbstractRosView.java
index 3a21b27..fe5aee6 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/AbstractRosView.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/AbstractRosView.java
@@ -16,6 +16,7 @@
 import org.eclipse.jface.action.Separator;
 import org.eclipse.jface.dialogs.IDialogSettings;
 import org.eclipse.tracecompass.incubator.internal.ros.ui.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.actions.HideRosoutAction;
 import org.eclipse.tracecompass.tmf.ui.views.timegraph.BaseDataProviderTimeGraphView;
 import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphPresentationProvider;
 import org.eclipse.ui.IWorkbenchActionConstants;
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/Messages.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/Messages.java
index 1d22a6c..40701fb 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/Messages.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/Messages.java
@@ -19,11 +19,6 @@
 public class Messages extends NLS {
     private static final String BUNDLE_NAME = "org.eclipse.tracecompass.incubator.internal.ros.ui.views.messages"; //$NON-NLS-1$
 
-    /** Name text for HideRosoutAction */
-    public static String HideRosoutAction_NameText;
-    /** Tooltip text for HideRosoutAction */
-    public static String HideRosoutAction_ToolTipText;
-
     /** Connection channel type */
     public static String AbstractRosPresentationProvider_ConnectionChannelType;
     /** Connections local host */
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/connections/RosConnectionsView.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/connections/RosConnectionsView.java
index 308f61f..c29388f 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/connections/RosConnectionsView.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/connections/RosConnectionsView.java
@@ -9,6 +9,7 @@
 
 package org.eclipse.tracecompass.incubator.internal.ros.ui.views.connections;
 
+import org.eclipse.jface.action.IToolBarManager;
 import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.connections.RosConnectionsDataProvider;
 import org.eclipse.tracecompass.incubator.internal.ros.ui.views.AbstractRosView;
 
@@ -29,6 +30,11 @@
         super(ID_SUFFIX, new RosConnectionsPresentationProvider(), RosConnectionsDataProvider.getFullDataProviderId());
     }
 
+    @Override
+    protected void fillLocalToolBar(IToolBarManager manager) {
+        super.fillLocalToolBar(manager);
+    }
+
     /**
      * @return the full view ID for this view
      */
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/Messages.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/Messages.java
new file mode 100644
index 0000000..829cfbf
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/Messages.java
@@ -0,0 +1,33 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.views.messageflow;
+
+import org.eclipse.osgi.util.NLS;
+
+/**
+ * Messages for ROS message flow UI
+ *
+ * @author Christophe Bedard
+ */
+public class Messages extends NLS {
+
+    private static final String BUNDLE_NAME = "org.eclipse.tracecompass.incubator.internal.ros.ui.views.messageflow.messages"; //$NON-NLS-1$
+
+    /** Multiple states for an event */
+    public static String RosMessageFlowPresentationProvider_MultipleStates;
+
+    static {
+        // initialize resource bundle
+        NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+    }
+
+    private Messages() {
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowPresentationProvider.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowPresentationProvider.java
new file mode 100644
index 0000000..0803dd8
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowPresentationProvider.java
@@ -0,0 +1,118 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.views.messageflow;
+
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.tracecompass.tmf.core.presentation.RGBAColor;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.StateItem;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphPresentationProvider;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEventStyleStrings;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeEvent;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * ROS Message Flow presentation provider
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowPresentationProvider extends TimeGraphPresentationProvider {
+
+    private static String ARROW_STATE = "arrow"; //$NON-NLS-1$
+
+    /**
+     * The enumeration of possible states for the view
+     */
+    public enum State {
+        /** Publisher queue segment */
+        PUB_QUEUE_SEGMENT(new RGB(0x3c, 0xb4, 0x4b)),
+        /** Subscriber queue segment */
+        SUB_QUEUE_SEGMENT(new RGB(0x43, 0x63, 0xd8)),
+        /** Callback segment */
+        CALLBACK_SEGMENT(new RGB(0xf5, 0x82, 0x31)),
+        /** Other segment */
+        UNKNOWN_SEGMENT(new RGB(0x40, 0x3b, 0x33)),
+        /** Network arrow (pub queue --> sub queue) */
+        NETWORK_ARROW(new RGB(0x26, 0x26, 0x26)),
+        /** Callback scheduling arrow (sub queue --> callback) */
+        CALLBACK_SCHEDULING_ARROW(new RGB(0x66, 0x66, 0x66)),
+        /** Publishing arrow (anything --> pub queue) */
+        PUBLISH_ARROW(new RGB(0xa6, 0xa6, 0xa6)),
+        /** Other arrow */
+        UNKNOWN_ARROW(new RGB(0xcc, 0xcc, 0xcc));
+
+        /** RGB color associated with a state */
+        public final RGB rgb;
+
+        private State(RGB rgb) {
+            this.rgb = rgb;
+        }
+    }
+
+    private static final StateItem[] STATE_TABLE;
+
+    static {
+        STATE_TABLE = new StateItem[State.values().length];
+        for (int i = 0; i < STATE_TABLE.length; ++i) {
+            State state = State.values()[i];
+
+            RGB stateColor = state.rgb;
+            String stateType = state.name().toLowerCase().contains(ARROW_STATE) ? ITimeEventStyleStrings.linkType() : ITimeEventStyleStrings.stateType();
+            ImmutableMap<String, Object> styleMap = ImmutableMap.of(
+                    ITimeEventStyleStrings.fillStyle(), ITimeEventStyleStrings.solidColorFillStyle(),
+                    ITimeEventStyleStrings.fillColor(), new RGBAColor(stateColor.red, stateColor.green, stateColor.blue).toInt(),
+                    ITimeEventStyleStrings.label(), String.valueOf(state.toString()),
+                    ITimeEventStyleStrings.itemTypeProperty(), stateType);
+            STATE_TABLE[i] = new StateItem(styleMap);
+        }
+    }
+
+    /**
+     * Constructor
+     */
+    public RosMessageFlowPresentationProvider() {
+        super(StringUtils.EMPTY);
+        // Nothing to do
+    }
+
+    @Override
+    public int getStateTableIndex(ITimeEvent event) {
+        if (event instanceof TimeEvent && ((TimeEvent) event).hasValue()) {
+            // Directly encoded in value
+            return ((TimeEvent) event).getValue();
+        }
+        return TRANSPARENT;
+    }
+
+    @Override
+    public StateItem[] getStateTable() {
+        return STATE_TABLE;
+    }
+
+    @Override
+    public @Nullable String getEventName(ITimeEvent event) {
+        if (event instanceof TimeEvent) {
+            TimeEvent ev = (TimeEvent) event;
+            return ev.getLabel();
+        }
+        return Messages.RosMessageFlowPresentationProvider_MultipleStates;
+    }
+
+    @Override
+    public @Nullable Map<String, String> getEventHoverToolTipInfo(ITimeEvent event) {
+        // TODO
+        return super.getEventHoverToolTipInfo(event);
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowView.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowView.java
new file mode 100644
index 0000000..0b422bf
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/RosMessageFlowView.java
@@ -0,0 +1,152 @@
+/**********************************************************************
+ * Copyright (c) 2019 École Polytechnique de Montréal
+ *
+ * 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
+ **********************************************************************/
+
+package org.eclipse.tracecompass.incubator.internal.ros.ui.views.messageflow;
+
+import java.util.Collections;
+import java.util.Objects;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.AbstractRosStateProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowAnalysis;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowDataProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.messageflow.RosMessageFlowSegmentEntryModel;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.model.messageflow.RosMessageFlowSegment.SegmentType;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.Activator;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.views.AbstractRosView;
+import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
+import org.eclipse.tracecompass.tmf.core.model.timegraph.ITimeGraphEntryModel;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
+import org.eclipse.tracecompass.tmf.core.signal.TmfStartAnalysisSignal;
+import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
+import org.eclipse.ui.IWorkbenchActionConstants;
+
+/**
+ * View for {@link RosMessageFlowAnalysis}
+ *
+ * @author Christophe Bedard
+ */
+public class RosMessageFlowView extends AbstractRosView {
+
+    /** View ID suffix */
+    public static final String ID_SUFFIX = "messageflow"; //$NON-NLS-1$
+
+    private static final String[] TREE_COLUMNS = new String[] { "Node", "Type", "Topic", "Segment type" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+
+    /**
+     * Constructor
+     */
+    public RosMessageFlowView() {
+        super(ID_SUFFIX, new RosMessageFlowPresentationProvider(), RosMessageFlowDataProvider.getFullDataProviderId());
+        setTreeColumns(TREE_COLUMNS);
+        setTreeLabelProvider(new RosMessageFlowTreeLabelProvider());
+        // Enable view filter dialog
+        setFilterColumns(new String[] { StringUtils.EMPTY });
+        setFilterLabelProvider(new RosViewFilterLabelProvider());
+    }
+
+    private static class RosMessageFlowTreeLabelProvider extends TreeLabelProvider {
+        @Override
+        public String getColumnText(Object element, int columnIndex) {
+            TimeGraphEntry entry = (TimeGraphEntry) element;
+            ITimeGraphEntryModel model = entry.getModel();
+            if (model instanceof RosMessageFlowSegmentEntryModel) {
+                RosMessageFlowSegmentEntryModel segmentModel = (RosMessageFlowSegmentEntryModel) model;
+                RosMessageFlowSegment segment = segmentModel.getSegment();
+                if (columnIndex >= 0 && columnIndex < TREE_COLUMNS.length) {
+                    if (columnIndex == 0) {
+                        return segment.getNodeName();
+                    } else if (columnIndex == 1) {
+                        return getQueueTypeName(segment.getType());
+                    } else if (columnIndex == 2) {
+                        return segment.getTopicName();
+                    } else if (columnIndex == 3) {
+                        return segment.getType().name();
+                    }
+                }
+            } else if (columnIndex == 0) {
+                // Experiment name
+                return entry.getName();
+            }
+
+            return StringUtils.EMPTY;
+        }
+    }
+
+    private static @Nullable String getQueueTypeName(SegmentType type) {
+        switch (type) {
+        case SUB_QUEUE: // Fallthrough
+        case SUB_CALLBACK:
+            return AbstractRosStateProvider.SUBSCRIBERS_LIST;
+        case PUB_QUEUE:
+            return AbstractRosStateProvider.PUBLISHERS_LIST;
+        case INVALID:
+        default:
+            return null;
+        }
+    }
+
+    private static class RosViewFilterLabelProvider extends TreeLabelProvider {
+        // Empty; only used to enable view filter dialog
+    }
+
+    @Override
+    protected void fillLocalToolBar(IToolBarManager manager) {
+        IDialogSettings settings = Objects.requireNonNull(Activator.getDefault()).getDialogSettings();
+        IDialogSettings section = settings.getSection(getClass().getName());
+        if (section == null) {
+            section = settings.addNewSection(getClass().getName());
+        }
+
+        IAction hideArrowsAction = getTimeGraphViewer().getHideArrowsAction(section);
+        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, hideArrowsAction);
+
+        // add a separator to local tool bar
+        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, new Separator());
+
+        super.fillLocalToolBar(manager);
+    }
+
+    /**
+     * Signal handler for analysis started, we need to rebuilt the entry list
+     * with updated statistics values for the current graph worker of the
+     * critical path module.
+     *
+     * @param signal
+     *            The signal
+     */
+    @TmfSignalHandler
+    public void analysisStarted(TmfStartAnalysisSignal signal) {
+        IAnalysisModule analysis = signal.getAnalysisModule();
+        if (analysis instanceof RosMessageFlowAnalysis) {
+            rebuild();
+        }
+    }
+
+    @Override
+    protected @NonNull Iterable<ITmfTrace> getTracesToBuild(@Nullable ITmfTrace trace) {
+        // The view only applies to the ROS experiment itself
+        return (trace != null) ? Collections.singleton(trace) : Collections.emptyList();
+    }
+
+    /**
+     * @return the full view ID
+     */
+    public static String getFullViewId() {
+        return AbstractRosView.ID_PREFIX + ID_SUFFIX;
+    }
+}
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/messages.properties b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/messages.properties
new file mode 100644
index 0000000..379a7ac
--- /dev/null
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messageflow/messages.properties
@@ -0,0 +1,11 @@
+###############################################################################
+# Copyright (c) 2019 École Polytechnique de Montréal
+#
+# 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
+#
+###############################################################################
+
+RosMessageFlowPresentationProvider_MultipleStates=multiple states
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messages.properties b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messages.properties
index 91ab8ca..d1aafbc 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messages.properties
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/messages.properties
@@ -8,9 +8,6 @@
 #
 ###############################################################################
 
-HideRosoutAction_NameText=Hide rosout
-HideRosoutAction_ToolTipText=Hide rosout publishers (which relay a node's logs to the rosout node) and the rosout node (which may redirect logs to stdout/stderr and a file)
-
 AbstractRosPresentationProvider_ConnectionChannelType=Channel type
 AbstractRosPresentationProvider_ConnectionHostLocal=Local
 AbstractRosPresentationProvider_ConnectionHostRemote=Remote
diff --git a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/queues/RosQueuesView.java b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/queues/RosQueuesView.java
index 59e2613..b6bb8d9 100644
--- a/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/queues/RosQueuesView.java
+++ b/tracetypes/org.eclipse.tracecompass.incubator.ros.ui/src/org/eclipse/tracecompass/incubator/internal/ros/ui/views/queues/RosQueuesView.java
@@ -9,9 +9,21 @@
 
 package org.eclipse.tracecompass.incubator.internal.ros.ui.views.queues;
 
+import java.util.Iterator;
+
 import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.AbstractRosStateProvider;
 import org.eclipse.tracecompass.incubator.internal.ros.core.analysis.queues.RosQueuesDataProvider;
+import org.eclipse.tracecompass.incubator.internal.ros.ui.actions.RosFollowMessageAction;
 import org.eclipse.tracecompass.incubator.internal.ros.ui.views.AbstractRosView;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
+import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
+import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
+import org.eclipse.ui.IWorkbenchActionConstants;
 
 /**
  * View for ROS Queues
@@ -23,6 +35,8 @@
     /** View ID suffix */
     public static final String ID_SUFFIX = "queues"; //$NON-NLS-1$
 
+    private RosFollowMessageAction fFollowMessageAction = null;
+
     /**
      * Constructor
      */
@@ -37,6 +51,75 @@
         // Empty; only used to enable view filter dialog
     }
 
+    @Override
+    protected void fillLocalToolBar(IToolBarManager manager) {
+        fFollowMessageAction = new RosFollowMessageAction(this, getTimeGraphViewer());
+        fFollowMessageAction.setEnabled(false);
+        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, fFollowMessageAction);
+
+        // add a separator to local tool bar
+        manager.appendToGroup(IWorkbenchActionConstants.MB_ADDITIONS, new Separator());
+        super.fillLocalToolBar(manager);
+    }
+
+    /**
+     * {@link TmfSignalHandler} for when the selection is updated
+     *
+     * @param signal
+     *            the selection range updated signal
+     */
+    @TmfSignalHandler
+    public synchronized void selection(TmfSelectionRangeUpdatedSignal signal) {
+        updateFollowMessageAction(signal.getSource());
+    }
+
+    private void updateFollowMessageAction(Object source) {
+        if (fFollowMessageAction != null) {
+            boolean isValid = false;
+            ITimeGraphEntry selection = getTimeGraphViewer().getSelection();
+            // FIXME an event is still selected in the view even if the focus is
+            // on another view, so it could still be considered valid
+            if (selection != null && source == this) {
+                ITimeEvent event = getMessageReferenceEvent(selection, getTimeGraphViewer().getSelectionBegin());
+                isValid = (event != null && event.getLabel() != null);
+            }
+            fFollowMessageAction.setEnabled(isValid);
+        }
+    }
+
+    /**
+     * Get the message event corresponding to an entry and timestamp
+     *
+     * @param selectedEntry
+     *            the selected entry
+     * @param timestamp
+     *            the selected timestamp
+     * @return the message event if found and valid, {@code null} otherwise
+     */
+    public static ITimeEvent getMessageReferenceEvent(ITimeGraphEntry selectedEntry, long timestamp) {
+        // It has to be a child of a queue entry
+        ITimeGraphEntry parent = selectedEntry.getParent();
+        if (parent == null || !parent.getName().equals(AbstractRosStateProvider.QUEUE)) {
+            return null;
+        }
+
+        // Find the corresponding message by comparing timestamps
+        ITimeEvent refEvent = null;
+        Iterator<@NonNull ? extends ITimeEvent> timeEventsIt = selectedEntry.getTimeEventsIterator();
+        while (timeEventsIt.hasNext()) {
+            ITimeEvent event = timeEventsIt.next();
+            long duration = event.getDuration();
+            long time = event.getTime();
+            if (duration > 0 && (time <= timestamp && timestamp < (time + duration))) {
+                refEvent = event;
+                break;
+            } else if (time > timestamp) {
+                break;
+            }
+        }
+        return refEvent;
+    }
+
     /**
      * @return the full view ID for this view
      */