/*******************************************************************************
* Copyright (c) 2015 É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
*
* Contributors:
* Francis Giraldeau - Initial implementation and API
* Geneviève Bastien - Initial implementation and API
*******************************************************************************/
package org.eclipse.tracecompass.analysis.graph.core.base;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfEdge.EdgeType;
import org.eclipse.tracecompass.analysis.graph.core.base.TmfVertex.EdgeDirection;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
/**
* Undirected, unweighed, timed graph data type for dependencies between
* elements of a system.
*
* Vertices are timed: each vertex has a timestamp associated, so the vertex
* belongs to an object (the key of the multimap) at a given time. This is why
* we use a ListMultimap to represent the graph, instead of a simple list.
*
* @author Francis Giraldeau
* @author Geneviève Bastien
*/
public class TmfGraph {
private final ListMultimap<IGraphWorker, TmfVertex> fNodeMap;
private final Map<TmfVertex, IGraphWorker> fReverse;
/* Latch tracking if the graph is done building or not */
private final CountDownLatch fFinishedLatch = new CountDownLatch(1);
/**
* Constructor
*/
public TmfGraph() {
fNodeMap = NonNullUtils.checkNotNull(ArrayListMultimap.create());
fReverse = new HashMap<>();
}
/**
* Add node to the provided object without linking
*
* @param worker
* The key of the object the vertex belongs to
* @param vertex
* The new vertex
*/
public void add(IGraphWorker worker, TmfVertex vertex) {
List<TmfVertex> list = fNodeMap.get(worker);
list.add(vertex);
fReverse.put(vertex, worker);
}
/**
* Add node to object's list and make horizontal link with tail.
*
* @param worker
* The key of the object the vertex belongs to
* @param vertex
* The new vertex
* @return The edge constructed
*/
public @Nullable TmfEdge append(IGraphWorker worker, TmfVertex vertex) {
return append(worker, vertex, EdgeType.DEFAULT);
}
/**
* Add node to object's list and make horizontal link with tail.
*
* @param worker
* The key of the object the vertex belongs to
* @param vertex
* The new vertex
* @param type
* The type of edge to create
* @return The edge constructed
*/
public @Nullable TmfEdge append(IGraphWorker worker, TmfVertex vertex, EdgeType type) {
List<TmfVertex> list = fNodeMap.get(worker);
TmfVertex tail = getTail(worker);
TmfEdge link = null;
if (tail != null) {
link = tail.linkHorizontal(vertex);
link.setType(type);
}
list.add(vertex);
fReverse.put(vertex, worker);
return link;
}
/**
* Add a link between two vertices of the graph. The from vertex must be in
* the graph. If the 'to' vertex is not in the graph, it will be appended to
* the object the 'from' vertex is for. Otherwise a vertical or horizontal
* link will be created between the vertices.
*
* Caution: this will remove without warning any previous link from the
* 'from' vertex
*
* @param from
* The source vertex
* @param to
* The destination vertex
* @return The newly created edge
*/
public TmfEdge link(TmfVertex from, TmfVertex to) {
return link(from, to, EdgeType.DEFAULT);
}
/**
* Add a link between two vertices of the graph. The from vertex must be in
* the graph. If the 'to' vertex is not in the graph, it will be appended to
* the object the 'from' vertex is for. Otherwise a vertical or horizontal
* link will be created between the vertices.
*
* Caution: this will remove without warning any previous link from the
* 'from' vertex
*
* @param from
* The source vertex
* @param to
* The destination vertex
* @param type
* The type of edge to create
* @return The newly created edge
*/
public TmfEdge link(TmfVertex from, TmfVertex to, EdgeType type) {
IGraphWorker ofrom = fReverse.get(from);
IGraphWorker oto = fReverse.get(to);
if (ofrom == null) {
throw new IllegalArgumentException(Messages.TmfGraph_FromNotInGraph);
}
/* to vertex not in the graph, add it to ofrom */
if (oto == null) {
this.add(ofrom, to);
oto = ofrom;
}
TmfEdge link;
if (oto.equals(ofrom)) {
link = from.linkHorizontal(to);
} else {
link = from.linkVertical(to);
}
link.setType(type);
return link;
}
/**
* Returns tail node of the provided object
*
* @param worker
* The key of the object the vertex belongs to
* @return The last vertex of obj
*/
public @Nullable TmfVertex getTail(IGraphWorker worker) {
List<TmfVertex> list = fNodeMap.get(worker);
if (!list.isEmpty()) {
return list.get(list.size() - 1);
}
return null;
}
/**
* Removes the last vertex of the provided object
*
* @param worker
* The key of the object the vertex belongs to
* @return The removed vertex
*/
public @Nullable TmfVertex removeTail(IGraphWorker worker) {
List<TmfVertex> list = fNodeMap.get(worker);
if (!list.isEmpty()) {
TmfVertex last = list.remove(list.size() - 1);
fReverse.remove(last);
return last;
}
return null;
}
/**
* Returns head node of the provided object. This is the very first node of
* an object
*
* @param worker
* The key of the object the vertex belongs to
* @return The head vertex
*/
public @Nullable TmfVertex getHead(IGraphWorker worker) {
IGraphWorker ref = worker;
List<TmfVertex> list = fNodeMap.get(ref);
if (!list.isEmpty()) {
return list.get(0);
}
return null;
}
/**
* Returns the head node of the object of the nodeMap that has the earliest
* head vertex time
*
* @return The head vertex
*/
public @Nullable TmfVertex getHead() {
if (fNodeMap.isEmpty()) {
return null;
}
IGraphWorker headWorker = fNodeMap.keySet().stream()
.filter(k -> !fNodeMap.get(k).isEmpty())
.sorted((k1, k2) -> fNodeMap.get(k1).get(0).compareTo(fNodeMap.get(k2).get(0)))
.findFirst()
.get();
return getHead(headWorker);
}
/**
* Returns head vertex from a given node. That is the first of the current
* sequence of edges, the one with no left edge when going back through the
* original vertex's left edge
*
* @param vertex
* The vertex for which to get the head
* @return The head vertex from the requested vertex
*/
public TmfVertex getHead(TmfVertex vertex) {
TmfVertex headNode = vertex;
TmfEdge edge = headNode.getEdge(EdgeDirection.INCOMING_HORIZONTAL_EDGE);
while (edge != null) {
headNode = edge.getVertexFrom();
if (headNode == vertex) {
throw new CycleDetectedException();
}
edge = headNode.getEdge(EdgeDirection.INCOMING_HORIZONTAL_EDGE);
}
return headNode;
}
/**
* Returns all nodes of the provided object.
*
* @param obj
* The key of the object the vertex belongs to
* @return The list of vertices for the object
*/
public List<TmfVertex> getNodesOf(IGraphWorker obj) {
return fNodeMap.get(obj);
}
/**
* Returns the object the vertex belongs to
*
* @param node
* The vertex to get the parent for
* @return The object the vertex belongs to
*/
public @Nullable IGraphWorker getParentOf(TmfVertex node) {
return fReverse.get(node);
}
/**
* Returns the graph objects
*
* @return The vertex map
*/
public Set<IGraphWorker> getWorkers() {
return ImmutableSet.copyOf(fNodeMap.keySet());
}
/**
* Returns the number of vertices in the graph
*
* @return number of vertices
*/
public int size() {
return fReverse.size();
}
@Override
public String toString() {
return NonNullUtils.nullToEmptyString(String.format("Graph { actors=%d, nodes=%d }", //$NON-NLS-1$
fNodeMap.keySet().size(), fNodeMap.values().size()));
}
/**
* Dumps the full graph
*
* @return A string with the graph dump
*/
public String dump() {
StringBuilder str = new StringBuilder();
for (IGraphWorker obj : fNodeMap.keySet()) {
str.append(String.format("%10s ", obj)); //$NON-NLS-1$
str.append(fNodeMap.get(obj));
str.append("\n"); //$NON-NLS-1$
}
return NonNullUtils.nullToEmptyString(str.toString());
}
// ----------------------------------------------
// Graph operations and visits
// ----------------------------------------------
/**
* Visits a graph from the start vertex and every vertex of the graph having
* a path to/from them that intersects the start vertex
*
* Each time the worker changes, it goes back to the beginning of the
* current horizontal sequence and visits all nodes from there.
*
* Parts of the graph that are totally disjoints from paths to/from start
* will not be visited by this method
*
* @param start
* The vertex to start the scan for
* @param visitor
* The visitor
*/
public void scanLineTraverse(final @Nullable TmfVertex start, final ITmfGraphVisitor visitor) {
if (start == null) {
return;
}
Stack<TmfVertex> stack = new Stack<>();
HashSet<TmfVertex> visited = new HashSet<>();
stack.add(start);
while (!stack.isEmpty()) {
TmfVertex curr = stack.pop();
if (visited.contains(curr)) {
continue;
}
// process one line
TmfVertex n = getHead(curr);
visitor.visitHead(n);
while (true) {
visitor.visit(n);
visited.add(n);
// Only visit links up-right, guarantee to visit once only
TmfEdge edge = n.getEdge(EdgeDirection.OUTGOING_VERTICAL_EDGE);
if (edge != null) {
stack.push(edge.getVertexTo());
visitor.visit(edge, false);
}
edge = n.getEdge(EdgeDirection.INCOMING_VERTICAL_EDGE);
if (edge != null) {
stack.push(edge.getVertexFrom());
}
edge = n.getEdge(EdgeDirection.OUTGOING_HORIZONTAL_EDGE);
if (edge != null) {
visitor.visit(edge, true);
n = edge.getVertexTo();
} else {
// end of the horizontal list
break;
}
}
}
}
/**
* @see TmfGraph#scanLineTraverse(TmfVertex, ITmfGraphVisitor)
*
* @param start
* The worker from which to start the scan
* @param visitor
* The visitor
*/
public void scanLineTraverse(@Nullable IGraphWorker start, final ITmfGraphVisitor visitor) {
if (start == null) {
return;
}
scanLineTraverse(getHead(start), visitor);
}
/**
* Return the vertex for an object at a given timestamp, or the first vertex
* after the timestamp
*
* @param startTime
* The desired time
* @param worker
* The object for which to get the vertex
* @return Vertex at timestamp or null if no vertex at or after timestamp
*/
public @Nullable TmfVertex getVertexAt(ITmfTimestamp startTime, IGraphWorker worker) {
List<TmfVertex> list = fNodeMap.get(worker);
long ts = startTime.getValue();
// Scan the list until vertex is later than time
for (TmfVertex vertex : list) {
if (vertex.getTs() >= ts) {
return vertex;
}
}
return null;
}
/**
* Returns whether the graph is completed or not
*
* @return whether the graph is done building
*/
public boolean isDoneBuilding() {
return fFinishedLatch.getCount() == 0;
}
/**
* Countdown the latch to show that the graph is done building
*/
public void closeGraph() {
fFinishedLatch.countDown();
}
}