
org.threadly.concurrent.wrapper.statistics.ExecutorStatisticWrapper Maven / Gradle / Ivy
package org.threadly.concurrent.wrapper.statistics;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.LongAdder;
import org.threadly.concurrent.AbstractSubmitterExecutor;
import org.threadly.concurrent.RunnableCallableAdapter;
import org.threadly.concurrent.RunnableContainer;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.concurrent.statistics.StatisticExecutor;
import org.threadly.util.ArgumentVerifier;
import org.threadly.util.Clock;
import org.threadly.util.Pair;
import org.threadly.util.StatisticsUtils;
/**
* Wrap an {@link Executor} to get statistics based off executions through this wrapper. If
* statistics are desired on the {@link org.threadly.concurrent.PriorityScheduler},
* {@link org.threadly.concurrent.statistics.PrioritySchedulerStatisticTracker} may be a better
* option, taking advantages by extending and replacing logic rather than wrapping and just adding
* logic. Similarly
* {@link org.threadly.concurrent.statistics.SingleThreadSchedulerStatisticTracker} and
* {@link org.threadly.concurrent.statistics.NoThreadSchedulerStatisticTracker} should be used as
* an alternative for their respective schedulers.
*
* @since 4.6.0 (since 4.5.0 at org.threadly.concurrent.statistics)
*/
public class ExecutorStatisticWrapper extends AbstractSubmitterExecutor
implements StatisticExecutor {
private final Executor executor;
private final StatsContainer statsContainer;
/**
* Constructs a new statistics tracker wrapper for a given executor. This constructor uses
* a sensible default for the memory usage of collected statistics.
*
* This defaults to inaccurate time. Meaning that durations and delays may under report (but
* NEVER OVER what they actually were). This has the least performance impact. If you want more
* accurate time consider using {@link #ExecutorStatisticWrapper(Executor, boolean)}.
*
* @param executor Executor to defer executions to
*/
public ExecutorStatisticWrapper(Executor executor) {
this(executor, false);
}
/**
* Constructs a new statistics tracker wrapper for a given executor. This constructor uses
* a sensible default for the memory usage of collected statistics.
*
* @param executor Executor to defer executions to
* @param accurateTime {@code true} to ensure that delays and durations are not under reported
*/
public ExecutorStatisticWrapper(Executor executor, boolean accurateTime) {
this(executor, 1000, accurateTime);
}
/**
* Constructs a new statistics tracker wrapper for a given executor.
*
* This defaults to inaccurate time. Meaning that durations and delays may under report (but
* NEVER OVER what they actually were). This has the least performance impact. If you want more
* accurate time consider using {@link #ExecutorStatisticWrapper(Executor, int, boolean)}.
*
* @param executor Executor to defer executions to
* @param maxStatisticWindowSize maximum number of samples to keep internally
*/
public ExecutorStatisticWrapper(Executor executor, int maxStatisticWindowSize) {
this(executor, maxStatisticWindowSize, false);
}
/**
* Constructs a new statistics tracker wrapper for a given executor.
*
* @param executor Executor to defer executions to
* @param maxStatisticWindowSize maximum number of samples to keep internally
* @param accurateTime {@code true} to ensure that delays and durations are not under reported
*/
public ExecutorStatisticWrapper(Executor executor,
int maxStatisticWindowSize, boolean accurateTime) {
ArgumentVerifier.assertGreaterThanZero(maxStatisticWindowSize, "maxStatisticWindowSize");
this.executor = executor;
this.statsContainer = new StatsContainer(maxStatisticWindowSize, accurateTime);
}
@Override
protected void doExecute(Runnable task) {
statsContainer.queuedTaskCount.increment();
executor.execute(new StatisticRunnable(task, Clock.accurateForwardProgressingMillis(),
statsContainer));
}
@Override
public List getExecutionDelaySamples() {
ArrayList runDelays;
synchronized (statsContainer.runDelays) {
runDelays = new ArrayList<>(statsContainer.runDelays);
}
return runDelays;
}
@Override
public double getAverageExecutionDelay() {
List delaySamples = getExecutionDelaySamples();
if (delaySamples.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(delaySamples);
}
@Override
public Map getExecutionDelayPercentiles(double ... percentiles) {
List samples = getExecutionDelaySamples();
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
@Override
public List getExecutionDurationSamples() {
ArrayList runDurations;
synchronized (statsContainer.runDurations) {
runDurations = new ArrayList<>(statsContainer.runDurations);
}
return runDurations;
}
@Override
public double getAverageExecutionDuration() {
List durationSamples = getExecutionDurationSamples();
if (durationSamples.isEmpty()) {
return -1;
}
return StatisticsUtils.getAverage(durationSamples);
}
@Override
public Map getExecutionDurationPercentiles(double ... percentiles) {
List samples = getExecutionDurationSamples();
if (samples.isEmpty()) {
samples.add(0L);
}
return StatisticsUtils.getPercentiles(samples, percentiles);
}
@Override
public List> getLongRunningTasks(long durationLimitMillis) {
List> result = new ArrayList<>();
if (statsContainer.accurateTime) {
// ensure clock is updated before loop
Clock.accurateForwardProgressingMillis();
}
for (Map.Entry, Long> e : statsContainer.runningTasks.entrySet()) {
if (Clock.lastKnownForwardProgressingMillis() - e.getValue() > durationLimitMillis) {
Runnable task = e.getKey().getRight();
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 (statsContainer.runningTasks.containsKey(e.getKey())) {
result.add(new Pair<>(task, stack));
}
}
}
return result;
}
@Override
public int getLongRunningTasksQty(long durationLimitMillis) {
int result = 0;
if (statsContainer.accurateTime) {
// ensure clock is updated before loop
Clock.accurateForwardProgressingMillis();
}
for (Long l : statsContainer.runningTasks.values()) {
if (Clock.lastKnownForwardProgressingMillis() - l > durationLimitMillis) {
result++;
}
}
return result;
}
/**
* Call to check how many tasks are queued waiting for execution. If provided an executor that
* allows task removal, removing tasks from that executor will cause this value to be
* inaccurate. A task which is submitted, and then removed (to prevent execution), will be
* forever seen as queued since this wrapper has no opportunity to know about such removals.
*
* @return Number of tasks still waiting to be executed.
*/
@Override
public int getQueuedTaskCount() {
return statsContainer.queuedTaskCount.intValue();
}
@Override
public long getTotalExecutionCount() {
return statsContainer.totalExecutionCount.sum();
}
@Override
public void resetCollectedStats() {
synchronized (statsContainer.runDelays) {
statsContainer.runDelays.clear();
}
synchronized (statsContainer.runDurations) {
statsContainer.runDurations.clear();
}
}
/**
* Runnable wrapper that will track task execution statistics.
*
* @since 4.5.0
*/
protected static class StatisticRunnable implements RunnableContainer, Runnable {
private final Runnable task;
private final long expectedRunTime;
private final StatsContainer statsContainer;
public StatisticRunnable(Runnable task, long expectedRunTime, StatsContainer statsContainer) {
this.task = task;
this.expectedRunTime = expectedRunTime;
this.statsContainer = statsContainer;
}
@Override
public void run() {
Pair taskPair = new Pair<>(Thread.currentThread(), task);
statsContainer.trackStart(taskPair, expectedRunTime);
try {
task.run();
} finally {
statsContainer.trackFinish(taskPair);
}
}
@Override
public Runnable getContainedRunnable() {
return task;
}
}
/**
* Class which contains and maintains the statistics collected by a statistic tracker.
*
* @since 4.5.0
*/
protected static class StatsContainer {
public final int maxStatisticWindowSize;
public final boolean accurateTime;
protected final LongAdder totalExecutionCount;
protected final LongAdder queuedTaskCount;
protected final Map, Long> runningTasks;
protected final Deque runDurations;
protected final Deque runDelays;
public StatsContainer(int maxStatisticWindowSize, boolean accurateTime) {
this.maxStatisticWindowSize = maxStatisticWindowSize;
this.accurateTime = accurateTime;
this.totalExecutionCount = new LongAdder();
this.queuedTaskCount = new LongAdder();
this.runningTasks = new ConcurrentHashMap<>();
this.runDurations = new ArrayDeque<>();
this.runDelays = new ArrayDeque<>();
}
public void trackStart(Pair taskPair, long expectedRunTime) {
// get start time before any operations for hopefully more accurate execution delay
long startTime = Clock.accurateForwardProgressingMillis();
queuedTaskCount.decrement();
totalExecutionCount.increment();
synchronized (runDelays) {
runDelays.addLast(startTime - expectedRunTime);
if (runDelays.size() > maxStatisticWindowSize) {
runDelays.removeFirst();
}
}
// get possibly newer time so we don't penalize stats tracking as duration
runningTasks.put(taskPair, Clock.lastKnownForwardProgressingMillis());
}
public void trackFinish(Pair taskPair) {
long runDuration = (accurateTime ?
Clock.accurateForwardProgressingMillis() : Clock.lastKnownForwardProgressingMillis()) -
runningTasks.remove(taskPair);
synchronized (runDurations) {
runDurations.addLast(runDuration);
if (runDurations.size() > maxStatisticWindowSize) {
runDurations.removeFirst();
}
}
}
}
}