package org.radargun.stages.cache.background; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.*; import java.util.stream.Collectors; import org.radargun.logging.Log; import org.radargun.logging.LogFactory; import org.radargun.reporting.Timeline; import org.radargun.state.ServiceListener; import org.radargun.state.SlaveState; import org.radargun.stats.Statistics; import org.radargun.stats.representation.OperationThroughput; import org.radargun.utils.TimeService; /** * Coordinator for collecting statistics from background stressor threads started by {@link org.radargun.stages.cache.background.BackgroundOpsManager}. * * @author Matej Cimbora <mcimbora@redhat.com> * @author Radim Vansa <rvansa@redhat.com> */ public final class BackgroundStatisticsManager implements ServiceListener { public static final String CACHE_SIZE = "Cache size"; private static final String PREFIX = "BackgroundStatistics."; private static final Log log = LogFactory.getLog(BackgroundStatisticsManager.class); private BackgroundOpsManager backgroundOpsManager; private long statsIterationDuration; private List<IterationStats> stats; private ScheduledFuture statsTask; private SizeThread sizeThread; private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); private BackgroundStatisticsManager() {} private BackgroundStatisticsManager(BackgroundOpsManager backgroundOpsManager, long statsIterationDuration) { this.backgroundOpsManager = backgroundOpsManager; this.statsIterationDuration = statsIterationDuration; } /** * Returns {@link org.radargun.stages.cache.background.BackgroundStatisticsManager} instance. Creates {@link org.radargun.stages.cache.background.BackgroundOpsManager} * internally, if not found in slave state. */ public static BackgroundStatisticsManager getOrCreateInstance(SlaveState slaveState, String name, long statsIterationDuration) { BackgroundStatisticsManager statisticsManager = getInstance(slaveState, name); if (statisticsManager == null) { statisticsManager = new BackgroundStatisticsManager(BackgroundOpsManager.getOrCreateInstance(slaveState, name), statsIterationDuration); slaveState.put(PREFIX + name, statisticsManager); } return statisticsManager; } public static BackgroundStatisticsManager getInstance(SlaveState slaveState, String name) { return (BackgroundStatisticsManager) slaveState.get(PREFIX + name); } public synchronized List<IterationStats> getStats() { List<IterationStats> statsToReturn = stats; stats = null; return statsToReturn; } public synchronized void startStats() { if (stats == null) { stats = new ArrayList<>(); } if (sizeThread == null) { sizeThread = new SizeThread(); sizeThread.start(); } if (statsTask == null) { statsTask = executor.scheduleAtFixedRate(new StatsTask(), 0, statsIterationDuration, TimeUnit.MILLISECONDS); } } public synchronized void stopStats() { if (statsTask != null) { statsTask.cancel(true); statsTask = null; } if (sizeThread != null) { log.debug("Interrupting size thread"); sizeThread.interrupt(); try { sizeThread.join(); } catch (InterruptedException e) { log.error("Interrupted while waiting for stat thread to end."); sizeThread.interrupt(); } sizeThread = null; } } @Override public void serviceDestroyed() { stopStats(); } private class StatsTask implements Runnable { public StatsTask() { gatherStats(); // throw away first stats } public void run() { stats.add(gatherStats()); } private IterationStats gatherStats() { Stressor[] threads = backgroundOpsManager.getThreadManager().getStressorThreads(); List<Statistics> stats; if (threads == null) { stats = Collections.EMPTY_LIST; } else { stats = Arrays.asList(threads).stream().filter(t -> t != null).map(t -> t.getStatsSnapshot(true)).collect(Collectors.toList()); } Timeline timeline = backgroundOpsManager.getSlaveState().getTimeline(); long now = TimeService.currentTimeMillis(); long cacheSize = sizeThread.getAndResetSize(); timeline.addValue(Timeline.Category.customCategory(CACHE_SIZE), new Timeline.Value(now, cacheSize)); if (stats.isEmpty()) { // add zero for all operations we've already reported for (Timeline.Category valueCategory : timeline.getValueCategories()) { if (valueCategory.getName().endsWith(" Throughput")) { timeline.addValue(valueCategory, new Timeline.Value(now, 0)); } } } else { Statistics aggregated = stats.stream().reduce(Statistics.MERGE).orElseThrow(() -> new IllegalStateException("No statistics!")); for (String operation : aggregated.getOperations()) { OperationThroughput throughput = aggregated.getRepresentation(operation, OperationThroughput.class); Timeline.Category category = Timeline.Category.customCategory(operation + " Throughput"); if (throughput != null && (throughput.gross != 0 || timeline.getValues(category) != null)) { timeline.addValue(category, new Timeline.Value(now, throughput.gross)); } } } log.trace(String.format("Adding iteration %d: %s.", BackgroundStatisticsManager.this.stats.size(), stats)); return new IterationStats(stats, cacheSize); } } /** * * Used for fetching cache size. If the size can't be fetched during one stat iteration, value 0 * will be used. * */ private class SizeThread extends Thread { private boolean getSize = true; private long size = -1; @Override public void run() { try { while (!isInterrupted()) { synchronized (this) { while (!getSize) { wait(100); } getSize = false; } if (backgroundOpsManager.getCacheInfo() != null && backgroundOpsManager.getLifecycle().isRunning()) { size = backgroundOpsManager.getCacheInfo().getOwnedSize(); } else { size = 0; } } } catch (InterruptedException e) { log.trace("SizeThread interrupted."); interrupt(); } } public synchronized long getAndResetSize() { long rSize = size; size = -1; getSize = true; notify(); return rSize; } } public static class IterationStats implements Serializable { public final List<Statistics> statistics; public final long cacheSize; private IterationStats(List<Statistics> statistics, long cacheSize) { this.statistics = statistics; this.cacheSize = cacheSize; } } }