package com.irnems; import com.rusefi.FileLog; import org.jetbrains.annotations.NotNull; import java.text.FieldPosition; import java.text.NumberFormat; import java.util.*; public final class Histograms { private static final String SQL_STATEMENT = "SQL-statement"; 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 total_stats = new HashMap<>(); private final long start_time = System.currentTimeMillis(); public final ThreadLocal local_stats = new ThreadLocal() { @Override protected LocalStats initialValue() { return new LocalStats(); } }; private final HashSet 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 dumpStats() { Collection values = takeAndResetSnapshot(); List al = new ArrayList<>(); al.addAll(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 getCurrentStatistics() { Collection snapshot = getCurrentSnapshot(); return sortAndAddTimes(new ArrayList<>(snapshot)); } private List sortAndAddTimes(List al) { Collections.sort(al, new Comparator() { public int compare(StatisticsGroup o1, StatisticsGroup o2) { return o1.type.compareTo(o2.type); } }); long time = System.currentTimeMillis(); List 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[]{"(", " [", "-", "-", "] ", ")"}; FileLog.rlog("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 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 getCurrentSnapshot() { List lss = getLocalStats(false); HashMap 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 takeAndResetSnapshot() { List lss = getLocalStats(true); HashMap snapshot = new HashMap<>(); for (LocalStats ls : lss) { HashMap 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 snapshot, HashMap 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 getLocalStats(boolean reset) { List 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(SQL_STATEMENT); Arrays.sort(sts, new Comparator() { 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 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[sg.data.size()]); 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 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 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; } } }