rusefi/java_console/ui/src/main/java/com/rusefi/Histograms.java

493 lines
16 KiB
Java

package com.rusefi;
import com.devexperts.logging.Logging;
import org.jetbrains.annotations.NotNull;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.util.*;
import static com.devexperts.logging.Logging.getLogging;
public final class Histograms {
private static final Logging log = getLogging(Histograms.class);
private static final String MAGIC = "total";
public static final double H_ACCURACY = 0.05;
public static final int BOUND_LENGTH = (int) (Math.log(Long.MAX_VALUE) / Math.log(1.0 + H_ACCURACY));
public static final long LONG_MAX_INT = 0x7fffffffffffffffL;
public static final double H_CONFIDENCE = 0.8;
public static final int SBI_SIZE = 1000;
// ======= Initialization =======
private final HashMap<String, StatisticsGroup> total_stats = new HashMap<>();
private final long start_time = System.currentTimeMillis();
public final ThreadLocal<LocalStats> local_stats = ThreadLocal.withInitial(LocalStats::new);
private final HashSet<LocalStats> all_local_stats = new HashSet<>();
private long last_dump = System.currentTimeMillis();
/**
* this method updates totals & resets snapshot
* todo: get rid of TOS 'Profiler' and make this method protected?
*
* @see #getCurrentStatistics()
*/
@NotNull
public List<String> dumpStats() {
Collection<StatisticsGroup> values = takeAndResetSnapshot();
List<StatisticsGroup> al = new ArrayList<>(values);
synchronized (total_stats) {
for (StatisticsGroup source : values) {
String type = source.type + ".TOTAL";
StatisticsGroup dest = total_stats.get(type);
if (dest == null)
total_stats.put(type, dest = new StatisticsGroup(type));
dest.add(source);
}
al.addAll(total_stats.values());
}
return sortAndAddTimes(al);
}
/**
* this method displays currently gathered histogram without affecting state
*
* @see #dumpStats()
*/
@SuppressWarnings("UnusedDeclaration")
@NotNull
public List<String> getCurrentStatistics() {
Collection<StatisticsGroup> snapshot = getCurrentSnapshot();
return sortAndAddTimes(new ArrayList<>(snapshot));
}
private List<String> sortAndAddTimes(List<StatisticsGroup> al) {
Collections.sort(al, new Comparator<StatisticsGroup>() {
public int compare(StatisticsGroup o1, StatisticsGroup o2) {
return o1.type.compareTo(o2.type);
}
});
long time = System.currentTimeMillis();
List<String> result = new ArrayList<>();
for (StatisticsGroup sg : al)
result.add(toString(sg, time - (sg.type.endsWith(".TOTAL") ? start_time : last_dump)));
last_dump = time;
return result;
}
// ========= Internal Data Structures ========
/**
* Accuracy of histogram - the ratio of bar width to its value.
*/
/**
* Confidence interval to be printed together with min/max interval.
*/
/**
* Confidence quantity bounds for logging.
*/
private final double[] confidence_bounds;
/**
* Confidence interval separators for logging.
*/
private final String[] confidence_separators;
/**
* Bounds of histogram bars. Bar number 'i' covers interval
* from bounds[i] inclusive to bounds[i + 1] exclusive.
*/
private final long[] bounds;
/**
* Index for direct access to intervals with small bounds.
*/
private final int[] small_bounds_index;
/**
* Construct new instance of profiler with specified parameters.
*/
public Histograms() {
confidence_bounds = new double[]{0.5 - H_CONFIDENCE * 0.5, 0.5, 0.5 + H_CONFIDENCE * 0.5};
confidence_separators = new String[]{"(", " [", "-", "-", "] ", ")"};
log.info("BOUND_LENGTH=" + BOUND_LENGTH);
bounds = new long[BOUND_LENGTH];
bounds[0] = 0;
for (int i = 1; i < BOUND_LENGTH; i++) {
long prev = bounds[i - 1];
long next = prev + (long) ((double) prev * H_ACCURACY);
if (next == prev) // Ensure minimum step for small numbers.
next = prev + 1;
if (next < prev) // Overflow over Long.MAX_VALUE occurred.
next = LONG_MAX_INT;
bounds[i] = next;
}
bounds[BOUND_LENGTH - 1] = LONG_MAX_INT;
small_bounds_index = new int[SBI_SIZE];
for (int i = 0, j = 0; j < SBI_SIZE; i++)
while (j < bounds[i + 1] && j < SBI_SIZE)
small_bounds_index[j++] = i;
}
/**
* Returns histogram index for specified value.
*/
public int getIndex(long value) {
if (value < 0)
return 0;
if (value < small_bounds_index.length)
return small_bounds_index[(int) value];
int l = small_bounds_index[small_bounds_index.length - 1];
int r = bounds.length - 1;
while (l < r) {
int m = (l + r) >> 1;
if (bounds[m] > value)
r = m - 1;
else if (bounds[m + 1] <= value)
l = m + 1;
else
return m;
}
return l;
}
public static final class LocalStats {
public HashMap<ValueType, StatisticsGroup> stats; // Type -> StatisticsGroup
}
/**
* Adds specified samples to statistics.
*/
public void addValue(ValueType t, String name, long value) {
String type = t.getName();
if (value < 0)
value = 0;
int index = getIndex(value);
LocalStats ls = local_stats.get();
boolean create;
create = ls.stats == null;
if (create)
ls.stats = new HashMap<>();
StatisticsGroup sg = ls.stats.get(t);
if (sg == null)
ls.stats.put(t, sg = new StatisticsGroup(type));
sg.add(name, index, value);
if (create)
synchronized (all_local_stats) {
all_local_stats.add(ls);
}
}
/**
* returns current statistics without resetting anything
*/
private Collection<StatisticsGroup> getCurrentSnapshot() {
List<LocalStats> lss = getLocalStats(false);
HashMap<String, StatisticsGroup> snapshot = new HashMap<>();
for (LocalStats ls : lss) {
// in case of a snapshot without reset, we have to merge under the lock
mergeStats(snapshot, ls.stats);
}
return snapshot.values();
}
/**
* Returns snapshot of all gathered statistics and clears them in process.
*/
private Collection<StatisticsGroup> takeAndResetSnapshot() {
List<LocalStats> lss = getLocalStats(true);
HashMap<String, StatisticsGroup> snapshot = new HashMap<>();
for (LocalStats ls : lss) {
HashMap<ValueType, StatisticsGroup> stats;
stats = ls.stats;
/**
* we will re-register this LocalStats on next #add() invocation
* @see #add(String, String, int, long)
*/
ls.stats = null;
mergeStats(snapshot, stats);
}
return snapshot.values();
}
private static void mergeStats(HashMap<String, StatisticsGroup> snapshot, HashMap<ValueType, StatisticsGroup> stats) {
if (stats != null)
for (StatisticsGroup source : stats.values()) {
StatisticsGroup dest = snapshot.get(source.type);
if (dest == null)
snapshot.put(source.type, source);
else
dest.add(source);
}
}
private List<LocalStats> getLocalStats(boolean reset) {
List<LocalStats> lss;
synchronized (all_local_stats) {
lss = new ArrayList<>(all_local_stats);
if (reset)
all_local_stats.clear();
}
return lss;
}
/**
* Sorts specified statistics of specified statistics group for logging.
*/
private static void sortStatistics(StatisticsGroup sg, Statistics[] sts) {
final boolean use_total = sg.type.startsWith(MAGIC);
Arrays.sort(sts, new Comparator<Statistics>() {
public int compare(Statistics st1, Statistics st2) {
if (use_total && st1.total_value != st2.total_value)
return st1.total_value < st2.total_value ? -1 : 1;
return st1.name.compareTo(st2.name);
}
});
}
// ========== Formatting Methods ==========
private static final double[] multipliers = {1.0, 10.0, 100.0};
private final NumberFormat formatter = NumberFormat.getNumberInstance();
private final FieldPosition field = new FieldPosition(0);
/**
* Formats specified value into specified string buffer. Not thread-safe.
*/
private StringBuffer format(double d, StringBuffer sb) {
int i = Math.abs(d) < 9.995 ? 2 : Math.abs(d) < 99.95 ? 1 : 0;
formatter.setMinimumFractionDigits(0);
formatter.setMaximumFractionDigits(i);
return formatter.format(Math.floor(d * multipliers[i] + 0.5) / multipliers[i], sb, field);
}
/**
* Appends header for specified statistics group for logging.
*/
private void appendHeader(StringBuffer sb, StatisticsGroup sg, long duration) {
double time = duration;
String time_scale = "ms";
if (time >= 24 * 3600 * 1000 * 0.995) {
time /= 24.0 * 3600.0 * 1000.0;
time_scale = "days";
} else if (time >= 3600 * 1000 * 0.995) {
time /= 3600.0 * 1000.0;
time_scale = "hours";
} else if (time >= 60 * 1000 * 0.995) {
time /= 60.0 * 1000.0;
time_scale = "min";
} else if (time >= 1000 * 0.995) {
time /= 1000.0;
time_scale = "sec";
}
sb.append(sg.type).append(" statistics for ");
format(time, sb).append(time_scale).append(" with ");
format(H_ACCURACY * 100.0, sb).append("% accuracy and ");
format(H_CONFIDENCE * 100.0, sb).append("% confidence:");
}
/**
* Appends specified statistics (main part) for logging.
*/
public void appendStatistics(StringBuffer sb, Statistics st, List<Long> report) {
format(st.total_value, sb).append(" / ").append(st.total_count).append(" = ");
format((double) st.total_value / (double) st.total_count, sb);
if (st.total_count < 0) {
sb.append("Total count is less then ZERO!\n");
return;
}
if (st.total_count <= 5) {
// There are too little samples for statistics.
sb.append(" are");
for (int j = 0; j < st.histogram.length; j++)
for (int k = 0; k < st.histogram[j]; k++) {
sb.append(' ');
format((bounds[j] + bounds[j + 1]) / 2, sb);
}
return;
}
// Print full min/confidence/max statistics.
sb.append(" in ").append(confidence_separators[0]);
int min = 0;
while (st.histogram[min] == 0)
min++;
// 'min' is index of interval with min sample.
format(bounds[min], sb).append(confidence_separators[1]);
report.add(bounds[min]);
long acc = 0;
// 'acc' is accumulated number of samples in [0, min - 1].
for (int j = 0; j < confidence_bounds.length; j++) {
long k = (long) Math.floor((double) st.total_count * confidence_bounds[j]);
// Always drop at least 1 'non-confident' sample...
if (k == 0)
k = 1;
if (k == st.total_count)
k = st.total_count - 1;
// 'k' is desired number of samples.
while (acc + st.histogram[min] < k)
acc += st.histogram[min++];
if (k < st.total_count / 2) // Converge to median (from left).
while (acc + st.histogram[min] <= k)
acc += st.histogram[min++];
// Now: acc <= k <= acc + st.histogram[min]
// And desired number of samples is within [min, min + 1)
double d = bounds[min];
if (acc != k)
d += (double) (bounds[min + 1] - 1 - bounds[min]) *
(double) (k - acc) /
(double) st.histogram[min];
format(d, sb).append(confidence_separators[j + 2]);
report.add((long)d);
}
int max = st.histogram.length - 1;
while (st.histogram[max] == 0)
max--;
// 'max' is index of interval with max sample.
long maxValue = bounds[max + 1] - 1;
format(maxValue, sb).append(confidence_separators[5]);
report.add(maxValue);
}
/**
* Formats specified statistics group for logging.
*/
private String toString(StatisticsGroup sg, long duration) {
StringBuffer sb = new StringBuffer(100 + sg.data.size() * 100);
appendHeader(sb, sg, duration);
Statistics[] sts = sg.data.values().toArray(new Statistics[0]);
sortStatistics(sg, sts);
for (Statistics st : sts) {
appendStatistics(sb, st, new ArrayList<>());
}
return sb.toString();
}
// ========== Internal Classes ==========
/**
* Performance statistics for single operation.
*/
public static class Statistics {
private static final long[] EMPTY_HISTOGRAM = new long[0];
public final String name;
public long total_value;
public long total_count;
public long[] histogram;
private Statistics(String name) {
this.name = name;
this.histogram = EMPTY_HISTOGRAM;
}
private Statistics(Statistics st) {
this.name = st.name;
this.total_value = st.total_value;
this.total_count = st.total_count;
this.histogram = st.histogram.length == 0 ? EMPTY_HISTOGRAM : st.histogram.clone();
}
public void add(int index, int count, long value) {
total_value += value;
total_count += count;
if (index >= histogram.length) {
int new_length = Math.max(histogram.length, 10);
while (index >= new_length)
new_length = new_length << 1;
long[] new_histogram = new long[new_length];
System.arraycopy(histogram, 0, new_histogram, 0, histogram.length);
histogram = new_histogram;
}
histogram[index] += count;
}
public void add(Statistics st) {
total_value += st.total_value;
total_count += st.total_count;
long[] hist = st.histogram;
if (histogram.length < st.histogram.length) {
hist = histogram;
histogram = st.histogram.clone();
}
for (int i = hist.length; --i >= 0; )
histogram[i] += hist[i];
}
}
/**
* Group of statistics of the same type.
*/
@SuppressWarnings("ForLoopReplaceableByForEach")
public static class StatisticsGroup {
public final String type;
public final HashMap<String, Statistics> data; // Maps statistics name (String) to Statistics.
private StatisticsGroup(String type) {
this.type = type;
this.data = new HashMap<>();
}
public void add(String name, int index, long value) {
Statistics st = data.get(name);
if (st == null)
data.put(name, st = new Statistics(name));
st.add(index, 1, value);
}
public void add(StatisticsGroup sg) {
for (Iterator<Statistics> it = sg.data.values().iterator(); it.hasNext(); ) {
Statistics source = it.next();
Statistics dest = data.get(source.name);
if (dest == null)
data.put(source.name, new Statistics(source));
else
dest.add(source);
}
}
}
/**
* The type of a value.
*/
public enum ValueType {
INVOCATION("Invocation");
/**
* Value type that is used to mean a method call.
*/
private final String name;
/**
* Gets the name of this {@link ValueType}.
*
* @return the name.
*/
public String getName() {
return name;
}
ValueType(String name) {
this.name = name;
}
}
}