package com.insightfullogic.honest_profiler.core.collector.lean; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import com.insightfullogic.honest_profiler.core.parser.LogEventListener; import com.insightfullogic.honest_profiler.core.parser.Method; import com.insightfullogic.honest_profiler.core.parser.StackFrame; import com.insightfullogic.honest_profiler.core.parser.ThreadMeta; import com.insightfullogic.honest_profiler.core.parser.TraceStart; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanNode; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanProfile; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanProfileListener; import com.insightfullogic.honest_profiler.core.profiles.lean.LeanThreadNode; import com.insightfullogic.honest_profiler.core.profiles.lean.info.FrameInfo; import com.insightfullogic.honest_profiler.core.profiles.lean.info.MethodInfo; import com.insightfullogic.honest_profiler.core.profiles.lean.info.ThreadInfo; /** * Collector which emits {@link LeanProfile}s, based on a request mechanism. * <p> * A {@link LeanProfile} will only be emitted when requested, or when the end of the log file is reached. When the final * {@link LeanProfile} has been emitted after a log file has been processed, requests no longer have any effect. * <p> * As long as no stacks have been received, nothing will be emitted. */ public class LeanLogCollector implements LogEventListener, ProfileSource { // Class Properties private static final long SECONDS_TO_NANOS = 1000 * 1000 * 1000; // Instance Properties private final LeanProfileListener listener; // Maps method ids to MethodInfo objects. private final Map<Long, MethodInfo> methodMap; // Maps thread ids to ThreadInfo objects. private final Map<Long, ThreadInfo> threadMap; // Maps thread ids to the profile trees for the threads. The root contains the Thread-level data, anything below are // stackframe-level data. private final Map<Long, LeanThreadNode> threadData; private Deque<StackFrame> stackTrace; // Seconds and nanos as reported by the last TraceStart received. private long prevSeconds; private long prevNanos; // Difference in ns between the previous and the current TraceStart. private long nanosSpent; // Indicates whether a profile was requested and should be emitted. private AtomicBoolean profileRequested; // Property for internal use. When a TraceStart is received, this is set to the LeanThreadNode corresponding to the // reported thread id. When stackframes are processed, it is replaced by the node representing the processed // stackframe. private LeanNode currentNode; // Indicates whether at least one stack has been processed. If not, no profile will be emitted. private boolean empty = true; // Instance Constructors /** * Constructor which sets the {@link LeanProfileListener} to which the {@link LeanProfile}s will be emitted. * <p> * @param listener the {@link LeanProfileListener} which will receive any emitted {@link LeanProfile}s */ public LeanLogCollector(final LeanProfileListener listener) { this.listener = listener; methodMap = new HashMap<>(); threadMap = new HashMap<>(); threadData = new HashMap<>(); stackTrace = new ArrayDeque<>(); currentNode = null; profileRequested = new AtomicBoolean(false); } // ProfileSource Implementation /** * Set a flag from any thread, which will cause an updated profile to be emitted as soon as possible. */ @Override public void requestProfile() { profileRequested.set(true); } // LogEventListener Implementation /** * Processes a {@link TraceStart}, i.e. the indication that a new stack will be coming in. The time is updated, and * the previously collected stack (if any) is processed. Then, the top-level {@link LeanThreadNode} is put in place * (possibly after creating it) using the thread id in the {@link TraceStart}. * <p> * A profile will be emitted if requested. */ @Override public void handle(TraceStart traceStart) { updateTime(traceStart.getTraceEpoch(), traceStart.getTraceEpochNano()); collectThreadDump(); currentNode = threadData .computeIfAbsent(traceStart.getThreadId(), v -> new LeanThreadNode()); emitProfileIfNeeded(); // The stacktrace should be empty anyway, given the collectThreadDump() logic, so got rid of this. // stackTrace.clear(); } /** * Processes a {@link StackFrame} by pushing it onto the {@link Deque}. */ @Override public void handle(StackFrame stackFrame) { stackTrace.push(stackFrame); } /** * Processes a {@link Method} which maps the method id to method information by putting the information into the * method map if it isn't there yet. */ @Override public void handle(Method newMethod) { methodMap.putIfAbsent(newMethod.getMethodId(), new MethodInfo(newMethod)); emitProfileIfNeeded(); } /** * Processes a {@link ThreadMeta} which maps the thread id to thread information by putting the information into the * method map if it isn't there yet, or updating the existing information using the thread name. * <p> * Sometimes several {@link ThreadMeta}s are received for the same thread id, but only the first contains the actual * thread name, which is why the update mechanism is in place. */ @Override public void handle(ThreadMeta newThreadMeta) { threadMap.compute( newThreadMeta.getThreadId(), (k, v) -> v == null ? new ThreadInfo(newThreadMeta) : v.checkAndSetName(newThreadMeta)); emitProfileIfNeeded(); } /** * Processes the "end of log" event, received when the end of a log file is reached. If no ThreadStart occurred * after the last stack frames were added, we obviously don't have an accurate "nanosSpent", but reusing the last * one should do fine as an approximation. */ @Override public void endOfLog() { collectThreadDump(); emitProfile(); } // Helper Methods /** * Processes the {@link StackFrame}s in the current stack. */ private void collectThreadDump() { // Slightly nicer IMHO than executing the "empty = false" inside the while loop. if (stackTrace.isEmpty()) { return; } while (!stackTrace.isEmpty()) { collectStackFrame(stackTrace.pop()); } empty = false; } /** * Processes a single {@link StackFrame}. The currentNode is the parent {@link LeanNode} which either represents the * parent {@link StackFrame} or, if this is the first {@link StackFrame} from the internal {@link Deque}, the parent * {@link LeanThreadNode} representing the thread for which the stack was received. The {@link StackFrame} * information will be aggregated into the children of the currentNode, and the resulting {@link LeanNode} becomes * the currentNode, acting as parent for the next {@link StackFrame} which will be processed, if there are any left * in the {@link Deque}. * <p> * @param stackFrame the {@link StackFrame} to be added as a child to the current {@link LeanNode} */ private void collectStackFrame(StackFrame stackFrame) { currentNode = currentNode.add(nanosSpent, new FrameInfo(stackFrame), stackTrace.isEmpty()); } /** * Calculates the number of ns spent between the previous {@link TraceStart} and the current one. After the first * {@link TraceStart} nanosSpent will still be 0. * <p> * @param newSeconds seconds reported in the current TraceStart * @param newNanos nanoSeconds reported in the current TraceStart */ private void updateTime(long newSeconds, long newNanos) { // The timestamp is absolute, so the very first time these calculations // are meaningless. And if the log doesn't contain timestamps, // prevSeconds will always be zero, so we avoid the calculations. if (prevSeconds > 0) { long secondsDiff = newSeconds - prevSeconds; long nanosDiff = newNanos - prevNanos; nanosSpent = (secondsDiff * SECONDS_TO_NANOS) + nanosDiff; } prevSeconds = newSeconds; prevNanos = newNanos; } /** * Emit a {@link LeanProfile} if a request is outstanding. */ private void emitProfileIfNeeded() { if (profileRequested.getAndSet(false)) { emitProfile(); } } /** * Emit a {@link LeanProfile} if at least one full stack was processed. */ private void emitProfile() { if (!empty) { listener.accept(new LeanProfile(methodMap, threadMap, threadData)); } } }