/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.toolkit.modules.statistics.internal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.toolkit.modules.concurrency.utils.ThreadsafeAutoCreationMap;
import de.rcenvironment.toolkit.modules.introspection.api.StatusCollectionContributor;
import de.rcenvironment.toolkit.modules.introspection.api.StatusCollectionRegistry;
import de.rcenvironment.toolkit.modules.statistics.api.CounterCategory;
import de.rcenvironment.toolkit.modules.statistics.api.StatisticsFilterLevel;
import de.rcenvironment.toolkit.modules.statistics.api.StatisticsTrackerService;
import de.rcenvironment.toolkit.modules.statistics.api.ValueEventCategory;
import de.rcenvironment.toolkit.modules.statistics.setup.StatisticsModuleConfiguration;
import de.rcenvironment.toolkit.modules.statistics.utils.CompactStacktraceBuilder;
import de.rcenvironment.toolkit.utils.internal.StringUtils;
import de.rcenvironment.toolkit.utils.text.TextLinesReceiver;
/**
* Default {@link StatisticsTrackerService} implementation.
*
* @author Robert Mischke
*/
public class StatisticsTrackerServiceImpl implements StatisticsTrackerService {
/**
* Default {@link CounterCategory} implementation.
*
* @author Robert Mischke
*/
public static final class CounterCategoryImpl implements CounterCategory {
private final ThreadsafeAutoCreationMap<String, AtomicLong> counters = new ThreadsafeAutoCreationMap<String, AtomicLong>() {
@Override
protected AtomicLong createNewEntry(String key) {
return new AtomicLong();
}
};
private final CompactStacktraceBuilder defaultCompactStacktraceBuilder;
public CounterCategoryImpl(CompactStacktraceBuilder defaultCompactStacktraceBuilder) {
this.defaultCompactStacktraceBuilder = defaultCompactStacktraceBuilder;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public void count(String key) {
counters.get(key).incrementAndGet();
}
@Override
public void count(String key, long delta) {
counters.get(key).addAndGet(delta);
}
@Override
public void countClass(Object object) {
if (object != null) {
count(object.getClass().getName());
} else {
count("<null>");
}
}
@Override
public void countStacktrace() {
count(defaultCompactStacktraceBuilder.getSingleLineStacktrace(1)); // "1" = skip this service method itself
}
}
/**
* Default {@link ValueEventCategory} implementation.
*
* @author Robert Mischke
*/
public static final class ValueEventCategoryImpl implements ValueEventCategory {
private final ThreadsafeAutoCreationMap<String, ValueTrackerEntry> valueTrackerEntries =
new ThreadsafeAutoCreationMap<String, StatisticsTrackerServiceImpl.ValueTrackerEntry>() {
@Override
protected ValueTrackerEntry createNewEntry(String key) {
return new ValueTrackerEntry();
}
};
@Override
public boolean isEnabled() {
return true;
}
@Override
public void registerEvent(String key, long value) {
valueTrackerEntries.get(key).register(value);
}
}
/**
* The {@link CounterCategory} implementation returned for disabled categories.
*
* @author Robert Mischke
*/
private static final class NoOperationCounterCategory implements CounterCategory {
@Override
public boolean isEnabled() {
return false;
}
@Override
public void countClass(Object object) {}
@Override
public void count(String key, long delta) {}
@Override
public void count(String key) {}
@Override
public void countStacktrace() {}
}
/**
* The {@link ValueEventCategory} implementation returned for disabled categories.
*
* @author Robert Mischke
*/
private static final class NoOperationValueEventCategory implements ValueEventCategory {
@Override
public boolean isEnabled() {
return false;
}
@Override
public void registerEvent(String key, long value) {}
}
/**
* A holder for simple statistics over an unknown number of values added over time.
*
* @author Robert Mischke
*/
private static final class ValueTrackerEntry {
private long n;
private long min = Long.MAX_VALUE;
private long max = Long.MIN_VALUE;
private double sum;
public synchronized void register(long value) {
min = Math.min(min, value);
max = Math.max(max, value);
sum += value;
n++;
}
public String render() {
if (n == 0) {
return "-";
}
return StringUtils.format("Total %,.2f, Average %,.2f, Min %,d, Max %,d, counted %,d times", sum, sum / n, min, max, n);
}
}
private static final CounterCategory NO_OPERATION_COUNTER = new NoOperationCounterCategory();
private static final ValueEventCategory NO_OPERATION_EVENT_TRACKER = new NoOperationValueEventCategory();
private static ThreadsafeAutoCreationMap<String, CounterCategoryImpl> counterMap;
private static ThreadsafeAutoCreationMap<String, ValueEventCategoryImpl> valueTrackerMap;
private final StatisticsFilterLevel globalFilterLevel;
private final Log log = LogFactory.getLog(getClass());
public StatisticsTrackerServiceImpl(StatisticsModuleConfiguration moduleConfiguration,
StatusCollectionRegistry statusCollectionRegistry) {
final CompactStacktraceBuilder defaultCompactStacktraceBuilder = moduleConfiguration.getDefaultCompactStacktraceBuilder();
globalFilterLevel = moduleConfiguration.getStatisticsFilterLevel();
counterMap = new ThreadsafeAutoCreationMap<String, CounterCategoryImpl>() {
@Override
protected CounterCategoryImpl createNewEntry(String key) {
return new CounterCategoryImpl(defaultCompactStacktraceBuilder);
}
};
valueTrackerMap = new ThreadsafeAutoCreationMap<String, ValueEventCategoryImpl>() {
@Override
protected ValueEventCategoryImpl createNewEntry(String key) {
return new ValueEventCategoryImpl();
}
};
statusCollectionRegistry.addContributor(new StatusCollectionContributor() {
@Override
public String getStandardDescription() {
return "Statistics";
}
@Override
public void printDefaultStateInformation(TextLinesReceiver receiver) {
receiver.addLines(getFullReportAsStandardTextRepresentation(""));
}
@Override
public String getUnfinishedOperationsDescription() {
return null;
}
@Override
public void printUnfinishedOperationsInformation(TextLinesReceiver receiver) {}
});
}
@Override
public CounterCategory getCounterCategory(String name) {
return getCounterCategory(name, StatisticsFilterLevel.RELEASE);
}
@Override
public CounterCategory getCounterCategory(String categoryName, StatisticsFilterLevel filterLevel) {
if (filterLevel.compareTo(globalFilterLevel) <= 0) {
return counterMap.get(categoryName); // automatically creates new entries
} else {
log.debug(StringUtils
.format("Returning a disabled receiver for category '%s' as it was requested "
+ "with level %s while the global level is %s", categoryName, filterLevel, globalFilterLevel));
return NO_OPERATION_COUNTER;
}
}
@Override
public ValueEventCategory getValueEventCategory(String name) {
return getValueEventCategory(name, StatisticsFilterLevel.RELEASE);
}
@Override
public ValueEventCategory getValueEventCategory(String categoryName, StatisticsFilterLevel filterLevel) {
if (filterLevel.compareTo(globalFilterLevel) <= 0) {
return valueTrackerMap.get(categoryName); // automatically creates new entries
} else {
log.debug(StringUtils
.format("Returning a disabled receiver for category '%s' as it was requested "
+ "with level %s while the global level is %s", categoryName, filterLevel, globalFilterLevel));
return NO_OPERATION_EVENT_TRACKER;
}
}
@Override
public Map<String, Map<String, String>> getFullReport() {
Map<String, Map<String, String>> result = new TreeMap<>(); // sort by category names
Map<String, CounterCategoryImpl> countersSnapshot = counterMap.getShallowCopy();
for (Map.Entry<String, CounterCategoryImpl> category : countersSnapshot.entrySet()) {
// TODO refactor
Map<String, AtomicLong> categoryCopy = category.getValue().counters.getShallowCopy();
final Map<String, String> renderedCategoryCounters = renderCategoryCounters(categoryCopy);
if (!renderedCategoryCounters.isEmpty()) {
result.put(category.getKey(), renderedCategoryCounters);
}
}
Map<String, ValueEventCategoryImpl> valueTrackersSnapshot = valueTrackerMap.getShallowCopy();
for (Entry<String, ValueEventCategoryImpl> category : valueTrackersSnapshot.entrySet()) {
// TODO refactor
Map<String, ValueTrackerEntry> valueTrackersCopy = category.getValue().valueTrackerEntries.getShallowCopy();
Map<String, String> renderedValueTrackers = renderValueTrackers(valueTrackersCopy);
if (!renderedValueTrackers.isEmpty()) {
Map<String, String> existingMap = result.put(category.getKey(), renderedValueTrackers);
if (existingMap != null) {
// the same category name was used for counters and value trackers; merge the two maps
renderedValueTrackers.putAll(existingMap);
}
}
}
return result;
}
@Override
public Map<String, String> getReportForCategory(String category) {
return renderCategoryCounters(counterMap.get(category).counters.getShallowCopy());
}
@Override
public List<String> getFullReportAsStandardTextRepresentation() {
return getFullReportAsStandardTextRepresentation("");
}
@Override
public List<String> getFullReportAsStandardTextRepresentation(String linePrefix) {
List<String> output = new ArrayList<>();
Map<String, Map<String, String>> report = getFullReport();
for (Map.Entry<String, Map<String, String>> category : report.entrySet()) {
output.add(StringUtils.format("%s%s", linePrefix, category.getKey()));
for (Map.Entry<String, String> entry : category.getValue().entrySet()) {
output.add(StringUtils.format("%s %s - %s", linePrefix, entry.getValue(), entry.getKey()));
}
}
return output;
}
private Map<String, String> renderCategoryCounters(Map<String, AtomicLong> categoryCopy) {
Map<String, String> categoryResult = new TreeMap<String, String>();
for (Map.Entry<String, AtomicLong> entry : categoryCopy.entrySet()) {
String rendered = StringUtils.format("%,d", entry.getValue().get());
categoryResult.put(entry.getKey(), rendered);
}
return categoryResult;
}
private Map<String, String> renderValueTrackers(Map<String, ValueTrackerEntry> categoryCopy) {
Map<String, String> categoryResult = new TreeMap<String, String>();
for (Entry<String, ValueTrackerEntry> entry : categoryCopy.entrySet()) {
String rendered = entry.getValue().render();
categoryResult.put(entry.getKey(), rendered);
}
return categoryResult;
}
}