package org.threadly.concurrent.statistics;
import java.util.ArrayList;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import org.threadly.concurrent.RunnableCallableAdapter;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.TaskPriority;
import org.threadly.concurrent.collections.ConcurrentArrayList;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.util.Clock;
import org.threadly.util.Pair;
import org.threadly.util.StatisticsUtils;
/**
* This class primarily holds the structures used to store the statistics. These can not be
* maintained in the parent class since sub classes need to be able to access them. This exists
* primarily to reduce code duplication between priority stats trackers (since java can only
* extend one class). But it also is important as we don't want our the holding class to prevent
* garbage collection due to held references.
*
* @since 4.5.0
*/
class PriorityStatisticManager {
protected final int maxWindowSize;
protected final boolean accurateTime;
protected final LongAdder totalHighPriorityExecutions;
protected final LongAdder totalLowPriorityExecutions;
protected final LongAdder totalStarvablePriorityExecutions;
protected final ConcurrentHashMap<Pair<Thread, TaskStatWrapper>, Long> runningTasks;
protected final ConcurrentArrayList<Long> starvablePriorityRunDurations;
protected final ConcurrentArrayList<Long> lowPriorityRunDurations;
protected final ConcurrentArrayList<Long> highPriorityRunDurations;
protected final ConcurrentArrayList<Long> starvablePriorityExecutionDelay;
protected final ConcurrentArrayList<Long> lowPriorityExecutionDelay;
protected final ConcurrentArrayList<Long> highPriorityExecutionDelay;
protected PriorityStatisticManager(int maxWindowSize, boolean accurateTime) {
this.maxWindowSize = maxWindowSize;
this.accurateTime = accurateTime;
totalHighPriorityExecutions = new LongAdder();
totalLowPriorityExecutions = new LongAdder();
totalStarvablePriorityExecutions = new LongAdder();
runningTasks = new ConcurrentHashMap<>();
starvablePriorityRunDurations = new ConcurrentArrayList<>(0, maxWindowSize);
lowPriorityRunDurations = new ConcurrentArrayList<>(0, maxWindowSize);
highPriorityRunDurations = new ConcurrentArrayList<>(0, maxWindowSize);
starvablePriorityExecutionDelay = new ConcurrentArrayList<>(0, maxWindowSize);
lowPriorityExecutionDelay = new ConcurrentArrayList<>(0, maxWindowSize);
highPriorityExecutionDelay = new ConcurrentArrayList<>(0, maxWindowSize);
}
/**
* Get raw collection for storing execution durations.
*
* @param priority TaskPriority to look up against, can not be {@code null}
* @return Collection of execution duration statistics
*/
ConcurrentArrayList<Long> getExecutionDurationSamplesInternal(TaskPriority priority) {
switch (priority) {
case High:
return highPriorityRunDurations;
case Low:
return lowPriorityRunDurations;
case Starvable:
return starvablePriorityRunDurations;
default:
throw new UnsupportedOperationException();
}
}
/**
* Get raw collection for storing execution delays.
*
* @param priority TaskPriority to look up against, can not be {@code null}
* @return Collection of execution delay statistics
*/
ConcurrentArrayList<Long> getExecutionDelaySamplesInternal(TaskPriority priority) {
switch (priority) {
case High:
return highPriorityExecutionDelay;
case Low:
return lowPriorityExecutionDelay;
case Starvable:
return starvablePriorityExecutionDelay;
default:
throw new UnsupportedOperationException();
}
}
/**
* Get the raw atomic for storing execution counts for a given priority.
*
* @param priority TaskPriority to look up against, can not be {@code null}
* @return AtomicLong to track executions
*/
protected LongAdder getExecutionCount(TaskPriority priority) {
switch (priority) {
case High:
return totalHighPriorityExecutions;
case Low:
return totalLowPriorityExecutions;
case Starvable:
return totalStarvablePriorityExecutions;
default:
throw new UnsupportedOperationException();
}
}
/**
* Called at the start of execution to track statistics around task execution.
*
* @param taskPair Wrapper that is about to be executed
*/
protected void trackTaskStart(Pair<Thread, TaskStatWrapper> taskPair) {
getExecutionCount(taskPair.getRight().priority).increment();
runningTasks.put(taskPair, Clock.accurateForwardProgressingMillis());
}
/**
* Used to track how long tasks are tacking to complete.
*
* @param taskPair wrapper for task that completed
*/
protected void trackTaskFinish(Pair<Thread, TaskStatWrapper> taskPair) {
long finishTime = accurateTime ?
Clock.accurateForwardProgressingMillis() :
Clock.lastKnownForwardProgressingMillis();
ConcurrentArrayList<Long> runDurations = getExecutionDurationSamplesInternal(taskPair.getRight().priority);
Long startTime = runningTasks.remove(taskPair);
synchronized (runDurations.getModificationLock()) {
runDurations.add(finishTime - startTime);
trimWindow(runDurations);
}
}
/**
* Reduces the list size to be within the max window size.
* <p>
* Should have the list synchronized/locked before calling.
*
* @param list Collection to check size of and ensure is under max size
*/
@SuppressWarnings("rawtypes")
protected void trimWindow(Deque window) {
while (window.size() > maxWindowSize) {
window.removeFirst();
}
}
public List<Long> getExecutionDelaySamples() {
List<Long> resultList = new ArrayList<>(highPriorityExecutionDelay);
resultList.addAll(lowPriorityExecutionDelay);
resultList.addAll(starvablePriorityExecutionDelay);
return resultList;
}
public List<Long> getExecutionDelaySamples(TaskPriority priority) {
if (priority == null) {
return getExecutionDelaySamples();
}
return new ArrayList<>(getExecutionDelaySamplesInternal(priority));
}
public double getAverageExecutionDelay() {
List<Long> resultList = getExecutionDelaySamples();
if (resultList.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(resultList);
}
public double getAverageExecutionDelay(TaskPriority priority) {
if (priority == null) {
return getAverageExecutionDelay();
}
List<Long> stats = getExecutionDelaySamples(priority);
if (stats.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(stats);
}
public Map<Double, Long> getExecutionDelayPercentiles(double... percentiles) {
List<Long> samples = getExecutionDelaySamples();
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
public Map<Double, Long> getExecutionDelayPercentiles(TaskPriority priority,
double... percentiles) {
List<Long> samples = getExecutionDelaySamples(priority);
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
public List<Long> getExecutionDurationSamples() {
List<Long> resultList = new ArrayList<>(highPriorityRunDurations);
resultList.addAll(lowPriorityRunDurations);
resultList.addAll(starvablePriorityRunDurations);
return resultList;
}
public List<Long> getExecutionDurationSamples(TaskPriority priority) {
if (priority == null) {
return getExecutionDurationSamples();
}
return new ArrayList<>(getExecutionDurationSamplesInternal(priority));
}
public double getAverageExecutionDuration() {
List<Long> runDurations = getExecutionDurationSamples();
if (runDurations.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(runDurations);
}
public double getAverageExecutionDuration(TaskPriority priority) {
List<Long> runDurations = getExecutionDurationSamples(priority);
if (runDurations.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(runDurations);
}
public Map<Double, Long> getExecutionDurationPercentiles(double... percentiles) {
List<Long> samples = getExecutionDurationSamples();
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
public Map<Double, Long> getExecutionDurationPercentiles(TaskPriority priority,
double... percentiles) {
List<Long> samples = getExecutionDurationSamples(priority);
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
public List<Pair<Runnable, StackTraceElement[]>> getLongRunningTasks(long durationLimitMillis) {
List<Pair<Runnable, StackTraceElement[]>> result = new ArrayList<>();
if (accurateTime) {
// ensure clock is updated before loop
Clock.accurateForwardProgressingMillis();
}
for (Map.Entry<Pair<Thread, TaskStatWrapper>, Long> e : runningTasks.entrySet()) {
if (Clock.lastKnownForwardProgressingMillis() - e.getValue() > durationLimitMillis) {
Runnable task = e.getKey().getRight().task;
if (task instanceof ListenableFutureTask) {
ListenableFutureTask<?> lft = (ListenableFutureTask<?>)task;
if (lft.getContainedCallable() instanceof RunnableCallableAdapter) {
RunnableCallableAdapter<?> rca = (RunnableCallableAdapter<?>)lft.getContainedCallable();
task = rca.getContainedRunnable();
}
}
StackTraceElement[] stack = e.getKey().getLeft().getStackTrace();
// verify still in collection after capturing stack
if (runningTasks.containsKey(e.getKey())) {
result.add(new Pair<>(task, stack));
}
}
}
return result;
}
public int getLongRunningTasksQty(long durationLimitMillis) {
int result = 0;
long now = accurateTime ?
Clock.accurateForwardProgressingMillis() :
Clock.lastKnownForwardProgressingMillis();
Iterator<Long> it = runningTasks.values().iterator();
while (it.hasNext()) {
Long startTime = it.next();
if (now - startTime >= durationLimitMillis) {
result++;
}
}
return result;
}
public void resetCollectedStats() {
for (TaskPriority p : TaskPriority.values()) {
getExecutionDelaySamplesInternal(p).clear();
getExecutionDurationSamplesInternal(p).clear();
}
}
public long getTotalExecutionCount() {
long result = 0;
for (TaskPriority p : TaskPriority.values()) {
result += getExecutionCount(p).sum();
}
return result;
}
public long getTotalExecutionCount(TaskPriority priority) {
if (priority == null) {
return getTotalExecutionCount();
}
return getExecutionCount(priority).sum();
}
/**
* Wrapper for {@link Runnable} for tracking statistics.
*
* @since 4.5.0
*/
protected static class TaskStatWrapper implements Runnable, RunnableContainer {
protected final PriorityStatisticManager statsManager;
protected final TaskPriority priority;
protected final Runnable task;
public TaskStatWrapper(PriorityStatisticManager statsManager, TaskPriority priority, Runnable toRun) {
this.statsManager = statsManager;
this.priority = priority;
this.task = toRun;
}
@Override
public void run() {
Pair<Thread, TaskStatWrapper> taskPair = new Pair<>(Thread.currentThread(), this);
statsManager.trackTaskStart(taskPair);
try {
task.run();
} finally {
statsManager.trackTaskFinish(taskPair);
}
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
}