/* * Copyright 2014 Luke Usherwood. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package net.bettyluke.util.swing.monitor; import java.util.EnumMap; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; class PeriodStatistics { public static final long MEDIUM_THRESHOLD_NANOS = TimeUnit.MILLISECONDS.toNanos(100); public static final long LONG_THRESHOLD_NANOS = TimeUnit.MILLISECONDS.toNanos(400); public enum Category { SHORT(MEDIUM_THRESHOLD_NANOS), MEDIUM(LONG_THRESHOLD_NANOS), LONG(Long.MAX_VALUE); public final long threshold; private Category(long thresh) { threshold = thresh; } public static Category fromNanos(long nanos) { for (Category category : Category.values()) { if (nanos <= category.threshold) { return category; } } throw new AssertionError("Impossible"); } } public static final class CategoryStats { public int count; public long nanos; public CategoryStats() { } public CategoryStats(CategoryStats other) { count = other.count; nanos = other.nanos; } public void accumulate(long additionalNanos) { ++ count; nanos += additionalNanos; } } public long reportingIntervalNanos; public EnumMap<Category, CategoryStats> statsMap = initStatsMap(); public long incompleteNanos = 0L; public long longestEvent = 0L; public StackTraceElement[] stack; private static EnumMap<Category, CategoryStats> initStatsMap() { EnumMap<Category, CategoryStats> map = new EnumMap<>(Category.class); for (Category cat : Category.values()) { map.put(cat, new CategoryStats()); } return map; } private static EnumMap<Category, CategoryStats> copy(Map<Category, CategoryStats> other) { EnumMap<Category, CategoryStats> map = new EnumMap<>(Category.class); for (Entry<Category, CategoryStats> entry : other.entrySet()) { map.put(entry.getKey(), new CategoryStats(entry.getValue())); } return map; } public PeriodStatistics(long reportingIntervalMs) { this.reportingIntervalNanos = TimeUnit.MILLISECONDS.toNanos(reportingIntervalMs); } public PeriodStatistics(PeriodStatistics other) { reportingIntervalNanos = other.reportingIntervalNanos; incompleteNanos = other.incompleteNanos; statsMap = copy(other.statsMap); longestEvent = other.longestEvent; stack = other.stack; } public CategoryStats getShort() { return statsMap.get(Category.SHORT); } public CategoryStats getMedium() { return statsMap.get(Category.MEDIUM); } public CategoryStats getLong() { return statsMap.get(Category.LONG); } public void recordElapsedNanos(long elapsed) { accumulate(elapsed, Category.fromNanos(elapsed)); } public void completeFinalEvent(long additionalNanos) { accumulate(incompleteNanos, Category.fromNanos(incompleteNanos + additionalNanos)); incompleteNanos = 0L; } public void merge(PeriodStatistics other) { reportingIntervalNanos += other.reportingIntervalNanos; incompleteNanos += other.incompleteNanos; for (Entry<Category, CategoryStats> entry : other.statsMap.entrySet()) { Category key = entry.getKey(); CategoryStats value = entry.getValue(); CategoryStats stat = statsMap.get(key); stat.count += value.count; stat.nanos += value.nanos; } if (longestEvent < other.longestEvent) { longestEvent = other.longestEvent; } } private void accumulate(long nanos, Category category) { statsMap.get(category).accumulate(nanos); if (longestEvent < nanos) { longestEvent = nanos; } } /** * Called when the time period after us ends with an EDT event still ongoing the whole way * through. When that happens, it's time to flag our "uncharged time" as long (and move on). */ public void accrueIncomplete() { statsMap.get(Category.LONG).accumulate(incompleteNanos); incompleteNanos = 0L; } public float percentBusy() { return ((float) totalBusyNanos()) / reportingIntervalNanos * 100f; } private long totalBusyNanos() { long sum = 0L; for (CategoryStats stats : statsMap.values()) { sum += stats.nanos; } return sum; } @Override public String toString() { StringBuilder result = new StringBuilder(String.format( "Busy for %d / %d ms (%.1f%%), Longest: %d ms\n" + " %5d short: %5d ms\n" + " %5d medium: %5d ms\n" + " %5d long: %5d ms", TimeUnit.NANOSECONDS.toMillis(totalBusyNanos()), TimeUnit.NANOSECONDS.toMillis(reportingIntervalNanos), percentBusy(), TimeUnit.NANOSECONDS.toMillis(longestEvent), statsMap.get(Category.SHORT).count, TimeUnit.NANOSECONDS.toMillis(statsMap.get(Category.SHORT).nanos), statsMap.get(Category.MEDIUM).count, TimeUnit.NANOSECONDS.toMillis(statsMap.get(Category.MEDIUM).nanos), statsMap.get(Category.LONG).count, TimeUnit.NANOSECONDS.toMillis(statsMap.get(Category.LONG).nanos))); if (stack != null) { result.append("\nAn example call-stack during the busy period:\n"); for (StackTraceElement element : stack) { result.append("\tat " + element + "\n"); } } return result.toString(); } }