package rocks.inspectit.ui.rcp.editor.tree.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.apache.commons.collections.CollectionUtils;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import io.opentracing.References;
import rocks.inspectit.shared.all.communication.data.InvocationSequenceData;
import rocks.inspectit.shared.all.tracing.data.Span;
import rocks.inspectit.shared.all.tracing.data.SpanIdent;
import rocks.inspectit.shared.all.util.ObjectUtils;
import rocks.inspectit.shared.cs.communication.comparator.DefaultDataComparatorEnum;
import rocks.inspectit.shared.cs.communication.data.InvocationSequenceDataHelper;
/**
* Model to easier represent spans in the tree view.
*
* @author Ivan Senic
*
*/
public final class TraceTreeData implements Comparable<TraceTreeData> {
/**
* Span that belongs to this data.
*/
private final Span span;
/**
* Parent.
*/
private TraceTreeData parent;
/**
* Children of the data.
*/
private List<TraceTreeData> children = new ArrayList<>(0);
/**
* Invocations that belong to this trace part.
*/
private List<InvocationSequenceData> invocations = new ArrayList<>(0);
/**
* Default constructor. User {@link #buildModel(Collection, Collection)}.
*
* @param span
* Span
*/
private TraceTreeData(Span span) {
if (null == span) {
throw new IllegalArgumentException("Span must not be null.");
}
this.span = span;
}
/**
* Builds the model form the collection of spans and invocations.
*
* @param spans
* Spans. Must include at least one root span. It's expected that all given spans
* belong to the same trace.
* @param invocations
* Invocation that belong to the same trace as given spans.
* @return {@link TraceTreeData} containing complete model.
*/
public static TraceTreeData buildModel(Collection<? extends Span> spans, Collection<InvocationSequenceData> invocations) {
Span root = null;
Multimap<Long, Span> parentToSpanMap = MultimapBuilder.hashKeys().arrayListValues().build();
for (Span span : spans) {
if (span.getSpanIdent().isRoot()) {
root = span;
} else {
parentToSpanMap.put(span.getSpanIdent().getParentId(), span);
}
}
if (null == root) {
throw new IllegalArgumentException("Can not construct model without the root.");
}
TraceTreeData rootData = new TraceTreeData(root);
combine(rootData, root, parentToSpanMap);
for (InvocationSequenceData invocation : invocations) {
rootData.processInvocation(invocation);
}
return rootData;
}
/**
* Combines the Spans from the map with the given trace tree data / span.
*
* @param rootData
* {@link TraceTreeData}
* @param ref
* Span contained in the rootData
* @param parentToSpanMap
* map
*/
private static void combine(TraceTreeData rootData, Span ref, Multimap<Long, Span> parentToSpanMap) {
Collection<Span> spans = parentToSpanMap.removeAll(ref.getSpanIdent().getId());
if (CollectionUtils.isNotEmpty(spans)) {
for (Span span : spans) {
TraceTreeData data = new TraceTreeData(span);
rootData.addChild(data);
combine(data, span, parentToSpanMap);
}
}
}
/**
* Collects all invocations in this trace data and it's children and add's it to the given list.
*
* @param root
* {@link TraceTreeData} to start from.
* @param list
* List to add invoc to.
* @return returns given list for easier connecting.
*/
public static List<InvocationSequenceData> collectInvocations(TraceTreeData root, List<InvocationSequenceData> list) {
if (null == root) {
return list;
}
list.addAll(root.getInvocations());
for (TraceTreeData child : root.getChildren()) {
collectInvocations(child, list);
}
return list;
}
/**
* Collects all spans in this trace data and it's children and add's it to the given list.
*
* @param root
* {@link TraceTreeData} to start from.
* @param list
* List to add spans to.
* @return returns given list for easier connecting.
*/
public static List<Span> collectSpans(TraceTreeData root, List<Span> list) {
if (null == root) {
return list;
}
list.add(root.getSpan());
for (TraceTreeData child : root.getChildren()) {
collectSpans(child, list);
}
return list;
}
/**
* Searches the {@link TraceTreeData} for the given span ident, going to the trace children in
* order to locate the ident.
*
* @param root
* {@link TraceTreeData} to start from.
* @param spanIdent
* ident
* @return Found data or <code>null</code>
*/
public static TraceTreeData getForSpanIdent(TraceTreeData root, SpanIdent spanIdent) {
if (null == root) {
return null;
}
if (root.hasSpanIdent(spanIdent)) {
return root;
}
for (TraceTreeData child : root.getChildren()) {
TraceTreeData containing = getForSpanIdent(child, spanIdent);
if (null != containing) {
return containing;
}
}
return null;
}
/**
* If this trace part is considered to be asynchronous.
*
* @return If this trace part is considered to be asynchronous.
*/
public boolean isConsideredAsync() {
return isParentFollowsFromCaller() || isFollowsFromCallee();
}
/**
* Returns true only if the parent of this trace data has a client span that has follows from
* reference.
*
* @return if parent is client span called with follows from reference to
*/
private boolean isParentFollowsFromCaller() {
return (null != parent) && parent.getSpan().isCaller() && References.FOLLOWS_FROM.equals(parent.getSpan().getReferenceType());
}
/**
* Returns true only if this trace data has a server span that has follows from reference.
*
* @return if data has server span called with follows from reference to
*/
private boolean isFollowsFromCallee() {
return !span.isCaller() && References.FOLLOWS_FROM.equals(span.getReferenceType());
}
/**
* Returns exclusive duration.
*
* @return Returns exclusive duration.
*/
public double getExclusiveDuration() {
double d = 0;
for (TraceTreeData child : children) {
d += child.getParentRelativeDuration();
}
double exclusive = span.getDuration() - d;
// ensure we never return negative values here
return Math.max(exclusive, 0d);
}
/**
* Returns exclusive duration.
*
* @return Returns exclusive duration.
*/
public double getExclusivePercentage() {
// go up in the tree
double d = span.getDuration();
TraceTreeData data = this;
while (((null != data.getParent()) && !data.isConsideredAsync())) {
d = data.getParent().getSpan().getDuration();
data = data.getParent();
}
return getExclusiveDuration() / d;
}
/**
* Returns the duration of this trace part relative to it's parent.
*
* @return Returns the duration of this trace part relative to it's parent.
*/
public double getParentRelativeDuration() {
if (isConsideredAsync()) {
return 0;
} else {
return span.getDuration();
}
}
/**
* Returns true if any of the spans in this {@link TraceTreeData} has given span ident.
*
* @param ident
* Trace tree data.
* @return Returns true if any of the spans in this {@link TraceTreeData} has given span ident.
*/
public boolean hasSpanIdent(SpanIdent ident) {
return Objects.equals(ident, span.getSpanIdent());
}
/**
* Returns true if any of the {@link #invocations} has nested sqls.
*
* @return Returns true if any of the {@link #invocations} has nested sqls.
*/
public boolean hasSqlsInInvocations() {
for (InvocationSequenceData invoc : invocations) {
if (InvocationSequenceDataHelper.hasNestedSqlStatements(invoc)) {
return true;
}
}
return false;
}
/**
* Returns true if any of the {@link #invocations} has nested sqls.
*
* @return Returns true if any of the {@link #invocations} has nested sqls.
*/
public boolean hasExceptionsInInvocations() {
for (InvocationSequenceData invoc : invocations) {
if (InvocationSequenceDataHelper.hasNestedExceptions(invoc)) {
return true;
}
}
return false;
}
/**
* Adds child to this trace data. Takes care of back-reference.
*
* @param data
* child
*/
private void addChild(TraceTreeData data) {
children.add(data);
data.setParent(this);
Collections.sort(children);
}
/**
* Processes the invocation by checking if the invocation belongs to this trace data. If not
* delegated to the {@link #children}.
*
* @param invocation
* {@link InvocationSequenceData}
* @return <code>true</code> if invocation was added, false otherwise
*/
private boolean processInvocation(InvocationSequenceData invocation) {
if (spanIdentMacthes(span, invocation.getSpanIdent())) {
addInvocation(invocation);
return true;
}
for (TraceTreeData child : children) {
if (child.processInvocation(invocation)) {
return true;
}
}
return false;
}
/**
* Adds one invocation to the {@link #invocations}.
*
* @param invocation
* {@link InvocationSequenceData}
*/
private void addInvocation(InvocationSequenceData invocation) {
invocations.add(invocation);
Collections.sort(invocations, DefaultDataComparatorEnum.TIMESTAMP);
}
/**
* Returns true if the span ident matches the span.
*
* @param span
* span
* @param spanIdent
* span ident
* @return Returns true if the span ident matches the span.
*/
private static boolean spanIdentMacthes(Span span, SpanIdent spanIdent) {
if (null != span) {
return Objects.equals(span.getSpanIdent(), spanIdent);
}
return false;
}
/**
* Gets {@link #parent}.
*
* @return {@link #parent}
*/
public TraceTreeData getParent() {
return this.parent;
}
/**
* Sets {@link #parent}.
*
* @param parent
* New value for {@link #parent}
*/
private void setParent(TraceTreeData parent) {
this.parent = parent;
}
/**
* Gets {@link #invocations}.
*
* @return {@link #invocations}
*/
public List<InvocationSequenceData> getInvocations() {
return this.invocations;
}
/**
* Gets {@link #children}.
*
* @return {@link #children}
*/
public List<TraceTreeData> getChildren() {
return this.children;
}
/**
* Gets {@link #span}.
*
* @return {@link #span}
*/
public Span getSpan() {
return this.span;
}
/**
* {@inheritDoc}
*/
@Override
public int compareTo(TraceTreeData o) {
return ObjectUtils.compare(this.span.getTimeStamp(), o.span.getTimeStamp());
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + ((this.span == null) ? 0 : this.span.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
TraceTreeData other = (TraceTreeData) obj;
if (this.span == null) {
if (other.span != null) {
return false;
}
} else if (!this.span.equals(other.span)) {
return false;
}
return true;
}
}