blob: f7891db655c0bc0fb3cc04967ef9f4ed3d0e06b5 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016, 2019 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph;
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.Map.Entry;
import java.util.Objects;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.profiling.core.callgraph.ICallGraphProvider;
import org.eclipse.tracecompass.analysis.profiling.core.callstack.CallStackAnalysis;
import org.eclipse.tracecompass.analysis.profiling.core.callstack.IFlameChartProvider;
import org.eclipse.tracecompass.analysis.timing.core.segmentstore.IAnalysisProgressListener;
import org.eclipse.tracecompass.analysis.timing.core.segmentstore.ISegmentStoreProvider;
import org.eclipse.tracecompass.internal.analysis.profiling.core.callstack.SymbolAspect;
import org.eclipse.tracecompass.segmentstore.core.ISegment;
import org.eclipse.tracecompass.segmentstore.core.ISegmentStore;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
import org.eclipse.tracecompass.tmf.core.segment.ISegmentAspect;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
/**
* Call stack analysis used to create a segment for each call function from an
* entry/exit event. It builds a segment tree from the state system. An example
* taken from the Fibonacci trace's callStack shows the structure of the segment
* tree given by this analysis:
*
* <pre>
* (Caller) main
* ↓↑
* (Callee) Fibonacci
* ↓↑ ↓↑
* Fibonacci Fibonacci
* ↓↑ ↓↑
* ... ...
* </pre>
*
* FIXME: Remove the implemented ISegmentStoreProvider interface at next major
* API break
*
* @author Sonia Farrah
*/
public class CallGraphAnalysis extends TmfAbstractAnalysisModule implements ISegmentStoreProvider, ICallGraphProvider {
/**
* ID
*/
public static final String ID = "org.eclipse.tracecompass.internal.analysis.profiling.core.callgraph.callgraphanalysis"; //$NON-NLS-1$
// ------------------------------------------------------------------------
// Attributes
// ------------------------------------------------------------------------
private final ListenerList<IAnalysisProgressListener> fListeners = new ListenerList<>(ListenerList.IDENTITY);
/**
* The List of thread nodes. Each thread has a virtual node having the root
* function as children
*/
private List<ThreadNode> fThreadNodes = new ArrayList<>();
private final @Nullable CallStackAnalysis fCallStackAnalysis;
/**
* Protected constructor, without the analysis
*/
protected CallGraphAnalysis() {
super();
fCallStackAnalysis = null;
}
/**
* Default constructor
*
* @param callStackAnalysis
* The callstack analysis this callgraph will be built upon
*/
public CallGraphAnalysis(CallStackAnalysis callStackAnalysis) {
super();
fCallStackAnalysis = callStackAnalysis;
}
@Override
public @NonNull String getHelpText() {
String msg = Messages.CallGraphAnalysis_Description;
return (msg != null) ? msg : super.getHelpText();
}
@Override
public @NonNull String getHelpText(@NonNull ITmfTrace trace) {
return getHelpText();
}
@Override
public boolean canExecute(ITmfTrace trace) {
/*
* FIXME: change to !Iterables.isEmpty(getDependentAnalyses()) when
* analysis dependencies work better
*/
return true;
}
/**
* @deprecated Use the {@link IFlameChartProvider}'s segment store instead
*/
@Override
@Deprecated
public @NonNull Iterable<@NonNull ISegmentAspect> getSegmentAspects() {
return Collections.singletonList(SymbolAspect.SYMBOL_ASPECT);
}
@Override
protected Iterable<IAnalysisModule> getDependentAnalyses() {
CallStackAnalysis callStackAnalysis = fCallStackAnalysis;
if (callStackAnalysis == null) {
throw new NullPointerException("If the analysis is not set, this method should not be called"); //$NON-NLS-1$
}
return Collections.singleton(callStackAnalysis);
}
@Override
protected boolean executeAnalysis(@Nullable IProgressMonitor monitor) {
ITmfTrace trace = getTrace();
if (monitor == null || trace == null) {
return false;
}
CallStackAnalysis callstackModule = fCallStackAnalysis;
if (callstackModule == null) {
return false;
}
callstackModule.schedule();
callstackModule.waitForCompletion(monitor);
// TODO:Look at updates while the state system's being built
String[] threadsPattern = callstackModule.getThreadsPattern();
String[] processesPattern = callstackModule.getProcessesPattern();
ITmfStateSystem ss = callstackModule.getStateSystem();
if (ss == null || !iterateOverStateSystem(ss, threadsPattern, processesPattern, monitor)) {
return false;
}
monitor.worked(1);
monitor.done();
return true;
}
/**
* Iterate over the process of the state system,then iterate over the
* different threads of each process.
*
* @param ss
* The state system
* @param threadsPattern
* The threads pattern
* @param processesPattern
* The processes pattern
* @param monitor
* The monitor
* @return true if completed successfully
*/
@VisibleForTesting
protected boolean iterateOverStateSystem(ITmfStateSystem ss, String[] threadsPattern, String[] processesPattern, IProgressMonitor monitor) {
List<Integer> processQuarks = ss.getQuarks(processesPattern);
Map<ThreadNode, List<Integer>> mainAttribs = new HashMap<>();
for (int processQuark : processQuarks) {
int processId = getProcessId(ss, processQuark, ss.getCurrentEndTime());
for (int threadQuark : ss.getQuarks(processQuark, threadsPattern)) {
int callStackQuark = ss.optQuarkRelative(threadQuark, CallStackAnalysis.CALL_STACK);
if (callStackQuark == ITmfStateSystem.INVALID_ATTRIBUTE) {
continue;
}
List<Integer> subAttributes = ss.getSubAttributes(callStackQuark, false);
if (subAttributes.isEmpty()) {
continue;
}
String threadName = ss.getAttributeName(threadQuark);
long threadId = getProcessId(ss, threadQuark, ss.getStartTime());
AbstractCalledFunction initSegment = CalledFunctionFactory.create(0, 0, -1, threadName, processId, null);
ThreadNode init = new ThreadNode(initSegment, 0, threadId);
fThreadNodes.add(init);
mainAttribs.put(init, subAttributes);
}
}
iterateOverCallStack2D(ss, mainAttribs, monitor);
return true;
}
/** A class that represents a time range for an interval or function */
private static class CallgraphRange {
private final long fStart;
private final long fEnd;
public CallgraphRange(long start, long end) {
fStart = start;
fEnd = end;
}
/* Is there a common range with the other range */
public boolean overlap(CallgraphRange other) {
if (fStart <= other.fEnd && fEnd >= other.fStart) {
return true;
}
return false;
}
/* Do 2 time ranges overlap or are they contiguous */
public boolean overlapOrContiguous(CallgraphRange other) {
// Do these overlap
if (overlap(other)) {
return true;
}
// Are they contiguous
if (fStart - 1 == other.fEnd || fEnd + 1 == other.fStart) {
return true;
}
return false;
}
/* Is the other range fully included in this range */
public boolean includes(CallgraphRange other) {
return (fStart <= other.fStart && fEnd >= other.fEnd);
}
/* Is the interval fully included in this range */
public boolean includes(ITmfStateInterval interval) {
return (fStart <= interval.getStartTime() && fEnd >= interval.getEndTime());
}
/*
* Get the callgraph range that represents the intersection with the
* other range
*/
public @Nullable CallgraphRange getIntersection(CallgraphRange other) {
if (fStart > other.fEnd || fEnd < other.fStart) {
return null;
}
return new CallgraphRange(Math.max(fStart, other.fStart), Math.min(fEnd, other.fEnd));
}
/*
* Return a range that is the union of this and the other range. It
* supposes that both ranges overlap or are contiguous
*/
public CallgraphRange getUnion(CallgraphRange other) {
return new CallgraphRange(Math.min(fStart, other.fStart), Math.max(fEnd, other.fEnd));
}
}
/** A class that associates a range with a function */
private static class FunctionCall {
CallgraphRange fRange;
AbstractCalledFunction fFunc;
public FunctionCall(CallgraphRange range, AbstractCalledFunction function) {
fRange = range;
fFunc = function;
}
}
/** Represent a callgraph level in the algorithm */
private static class CallGraphLevel {
private final ThreadNode fThreadNode;
private final List<CallgraphRange> fRanges = new ArrayList<>();
private final Map<AggregatedCalledFunction, FunctionCall> fAggregated = new HashMap<>();
private final List<ITmfStateInterval> fOrphanedIntervals = new ArrayList<>();
private final @Nullable CallGraphLevel fParent;
private final int fDepth;
private @Nullable CallGraphLevel fChild = null;
public CallGraphLevel(ThreadNode threadNode, int depth, @Nullable CallGraphLevel parent) {
fThreadNode = threadNode;
fDepth = depth;
fParent = parent;
}
public void addInterval(ITmfStateInterval interval) {
fOrphanedIntervals.add(interval);
}
public void setChild(CallGraphLevel childLvl) {
fChild = childLvl;
}
public void setCovered(CallgraphRange newRange) {
// Try to get the biggest range including the new one from all the
// others
List<CallgraphRange> toRemove = new ArrayList<>();
CallgraphRange addRange = newRange;
for (CallgraphRange range : fRanges) {
if (newRange.overlapOrContiguous(range)) {
addRange = addRange.getUnion(range);
toRemove.add(range);
}
}
for (CallgraphRange range : toRemove) {
fRanges.remove(range);
}
fRanges.add(addRange);
}
private @Nullable AggregatedCalledFunction findAggregated(CallgraphRange range) {
for (Entry<AggregatedCalledFunction, FunctionCall> entry : fAggregated.entrySet()) {
if (entry.getValue().fRange.includes(range)) {
return entry.getKey();
}
}
return null;
}
/* Find an aggregated call in the parent that spans this range */
public @Nullable AggregatedCalledFunction findParentAggregated(CallgraphRange range) {
CallGraphLevel parent = fParent;
if (parent == null) {
return fThreadNode;
}
return parent.findAggregated(range);
}
private boolean isRangeCovered(CallgraphRange range) {
for (CallgraphRange coveredRange : fRanges) {
if (coveredRange.includes(range)) {
return true;
}
}
return false;
}
/*
* Recursively adopt all children intervals. Return whether the range is
* fully covered or if there is still missing information
*/
public boolean recursiveCoverChildren(CallgraphRange range, AbstractCalledFunction function, AggregatedCalledFunction aggregated) {
CallGraphLevel child = fChild;
// Last level, coverage complete
if (child == null) {
return true;
}
// Set child's already covered ranges (nulls) as covered
for (CallgraphRange childRange : child.fRanges) {
CallgraphRange covered = range.getIntersection(childRange);
if (covered != null) {
setCovered(covered);
}
}
List<ITmfStateInterval> toRemove = new ArrayList<>();
// Look if any orphaned interval in the child is within range
for (ITmfStateInterval interval : child.fOrphanedIntervals) {
if (!range.includes(interval)) {
continue;
}
/*
* The interval is within range, process it
*
* Create a function and aggregated call site for it
*/
toRemove.add(interval);
CallgraphRange childRange = new CallgraphRange(interval.getStartTime(), interval.getEndTime());
AbstractCalledFunction childFunc = CalledFunctionFactory.create(childRange.fStart, childRange.fEnd + 1, fDepth + 1, Objects.requireNonNull(interval.getValue()), fThreadNode.getProcessId(), function);
AggregatedCalledFunction childAgg = new AggregatedCalledFunction(childFunc, aggregated);
/*
* Recursively try to adopt intervals in the child
*
* Is the child fully covered ?
*/
if (child.recursiveCoverChildren(childRange, childFunc, childAgg)) {
/*
* Yes, add the child to the current aggregated call and set
* this range as covered in both child and current level
*/
aggregated.addChild(childFunc, childAgg);
child.setCovered(childRange);
setCovered(childRange);
} else {
/* No, save the new aggregated call in the child */
child.fAggregated.put(childAgg, new FunctionCall(childRange, childFunc));
}
}
// Remove orphaned intervals that found a parent
child.fOrphanedIntervals.removeAll(toRemove);
return isRangeCovered(range);
}
public void tryToCompleteParentCoverage(CallgraphRange range) {
CallGraphLevel parent = fParent;
if (parent == null) {
return;
}
parent.tryToCompleteCoverage(range, this);
}
private void tryToCompleteCoverage(CallgraphRange range, CallGraphLevel child) {
List<AggregatedCalledFunction> toRemove = new ArrayList<>();
/*
* Look at all the aggregated function to see those that overlap the
* current range, there could be many in case the range represents a
* null interval
*/
for (Entry<AggregatedCalledFunction, FunctionCall> entry : fAggregated.entrySet()) {
CallgraphRange parentRange = entry.getValue().fRange;
if (parentRange.overlap(range)) {
/*
* This function overlaps the range, maybe the child
* coverage is complete now
*/
if (child.isRangeCovered(parentRange)) {
/*
* Function range fully covered in the child, add it to
* its parent now and try to complete the parent
*/
toRemove.add(entry.getKey());
AggregatedCalledFunction parent = findParentAggregated(parentRange);
if (parent == null) {
// The parent was already covered, probably because
// there were null values above, just ignore
continue;
}
parent.addChild(entry.getValue().fFunc, entry.getKey());
setCovered(parentRange);
tryToCompleteParentCoverage(parentRange);
}
}
}
for (AggregatedCalledFunction agg : toRemove) {
fAggregated.remove(agg);
}
}
public @Nullable FunctionCall getParentData(AggregatedCalledFunction parentCall) {
CallGraphLevel parent = fParent;
if (parent == null) {
return null;
}
return parent.fAggregated.get(parentCall);
}
public int getDepth() {
return fDepth;
}
public int getProcessId() {
return fThreadNode.getProcessId();
}
}
private static boolean iterateOverCallStack2D(ITmfStateSystem ss, Map<ThreadNode, List<Integer>> parentAttribs, IProgressMonitor monitor) {
try {
long start = ss.getStartTime();
long end = ss.getCurrentEndTime();
Map<Integer, CallGraphLevel> attribToLevel = new HashMap<>();
List<Integer> attributes = new ArrayList<>();
// Create the levels for all the thread nodes and attributes
for (Entry<ThreadNode, List<Integer>> entry : parentAttribs.entrySet()) {
ThreadNode threadNode = entry.getKey();
List<Integer> subAttributes = entry.getValue();
attributes.addAll(subAttributes);
CallGraphLevel prevLevel = null;
for (int i = 0; i < subAttributes.size(); i++) {
CallGraphLevel level = new CallGraphLevel(threadNode, i, prevLevel);
if (prevLevel != null) {
prevLevel.setChild(level);
}
prevLevel = level;
attribToLevel.put(subAttributes.get(i), level);
}
}
/*
* Do a 2D query, starting from the end of the state system, the
* intervals ending last (ie typically the ones of lower depth) will
* come first, though they are not sorted by end time per se, but as
* a general trend, the callstack will be parsed from the end.
*/
for (ITmfStateInterval interval : ss.query2D(attributes, end, start)) {
if (monitor.isCanceled()) {
return false;
}
CallGraphLevel level = attribToLevel.get(interval.getAttribute());
if (level == null) {
throw new NullPointerException("The level should not be null, we created it just before!"); //$NON-NLS-1$
}
long intervalStart = interval.getStartTime();
long intervalEnd = interval.getEndTime();
CallgraphRange range = new CallgraphRange(intervalStart, intervalEnd);
Object value = interval.getValue();
/* Is the interval null ? */
if (value == null) {
/*
* Yes, there is no function to process at this level so we
* set this range as covered
*/
level.setCovered(range);
} else {
/* No, this interval represents a called function */
/*
* Is there a parent aggregated site already for this
* function ?
*/
AggregatedCalledFunction parent = level.findParentAggregated(range);
if (parent == null) {
/* No, keep this interval for later and continue */
level.addInterval(interval);
continue;
}
/*
* Yes, create the function and aggregated callsite from
* this interval
*/
FunctionCall parentData = level.getParentData(parent);
AbstractCalledFunction function = CalledFunctionFactory.create(intervalStart, intervalEnd + 1, level.getDepth(), value, level.getProcessId(), (parentData == null) ? null : parentData.fFunc);
AggregatedCalledFunction aggregated = new AggregatedCalledFunction(function, parent);
/*
* See if there are any children intervals to process and
* add to this aggregated site
*/
/*
* Do we have all children information for this interval's
* function ?
*/
if (!level.recursiveCoverChildren(range, function, aggregated)) {
/*
* No, save this function to be completed later and
* continue
*/
level.fAggregated.put(aggregated, new FunctionCall(range, function));
continue;
}
/*
* Yes, add the current site to the parent and set this
* range as covered for the current level
*/
parent.addChild(function, aggregated);
level.setCovered(range);
}
/*
* See if we can complete the parent(s) with this new information
*/
level.tryToCompleteParentCoverage(range);
}
} catch (StateSystemDisposedException e) {
return false;
}
return true;
}
/**
* @deprecated Use the {@link IFlameChartProvider}'s segment store instead
*/
@Override
@Deprecated
public void addListener(@NonNull IAnalysisProgressListener listener) {
fListeners.add(listener);
}
/**
* @deprecated Use the {@link IFlameChartProvider}'s segment store instead
*/
@Override
@Deprecated
public void removeListener(@NonNull IAnalysisProgressListener listener) {
fListeners.remove(listener);
}
@Override
protected void canceling() {
// Do nothing
}
/**
* @deprecated Use the {@link IFlameChartProvider}'s segment store instead
*/
@Override
@Deprecated
public @Nullable ISegmentStore<@NonNull ISegment> getSegmentStore() {
return null;
}
/**
* Merged threadnodes
*
* @return the merged threadnodes
*/
public Collection<ThreadNode> getFlameGraph() {
AbstractCalledFunction initSegment = CalledFunctionFactory.create(0, 0, -1, "", 0, null); //$NON-NLS-1$
ThreadNode init = new ThreadNode(initSegment, 0, 0);
fThreadNodes.forEach(
tn -> tn.getChildren().forEach(
child -> init.addChild(initSegment, child.clone())));
return Collections.singleton(init);
}
/**
* List of thread nodes. Each thread has a virtual node having the root
* functions called as children.
*
* @return The thread nodes
*/
public List<ThreadNode> getThreadNodes() {
return ImmutableList.copyOf(fThreadNodes);
}
private static int getProcessId(ITmfStateSystem ss, int processQuark, long curTime) {
if (processQuark != ITmfStateSystem.ROOT_ATTRIBUTE) {
try {
ITmfStateInterval interval = ss.querySingleState(curTime, processQuark);
String processName = ss.getAttributeName(processQuark);
Object processValue = interval.getValue();
if (processValue != null && (processValue instanceof Integer || processValue instanceof Long)) {
return ((Number) processValue).intValue();
}
return Integer.parseInt(processName);
} catch (StateSystemDisposedException | NumberFormatException e) {
/* use default processId */
}
}
return -1;
}
}