package net.rustnotmc.client; import java.util.Arrays; import net.rustnotmc.api.client.FrameStats; import net.rustnotmc.loader.Log; /** * Counts how fast the frames are going, and how often they do not. * * The average is what people quote and what hides the stutter. A client that * runs at sixty frames a second and drops to ten twice a second still averages * sixty, and feels like a slideshow. So the window keeps every frame time and * reports the slow tail as well: the median, the one percent low and the tenth * of a percent low. * * The frame time is measured between the frames, which is the whole point: it * includes the waiting, so a client that is fast at drawing but slow at * getting around to drawing again is caught by it. */ public final class RsxFrameStats { /** A second of frames, with room to spare. */ private static final int MAX_FRAMES = 4096; private static final long[] TIMES = new long[MAX_FRAMES]; private static int count; private static long windowStart; private static long lastFrameAt; private static long totalFrames; private static volatile FrameStats latest = FrameStats.EMPTY; private RsxFrameStats() { } /** * Called once per drawn frame. * * The first frame only starts the clock: there is nothing to compare it * against yet. */ public static void frame() { long now = System.nanoTime(); if (lastFrameAt != 0L) { long delta = now - lastFrameAt; // a frame longer than a second is a stall, a pause or a breakpoint, // and putting it in the window would only say that the machine // stopped for a moment if (delta > 0L && delta < 1000000000L && count < MAX_FRAMES) { TIMES[count++] = delta; } } lastFrameAt = now; totalFrames++; if (windowStart == 0L) { windowStart = now; return; } long elapsed = now - windowStart; if (elapsed < 1000000000L) { return; } close(elapsed); } /** Ends the window, works out the numbers and writes them down. */ private static void close(long elapsedNanos) { long[] sorted = Arrays.copyOf(TIMES, count); Arrays.sort(sorted); double seconds = elapsedNanos / 1000000000.0D; double average = count == 0 ? 0.0D : count / seconds; double median = percentile(sorted, 0.50D); double onePercent = percentile(sorted, 0.99D); double tenthPercent = percentile(sorted, 0.999D); double worst = count == 0 ? 0.0D : sorted[count - 1]; FrameStats stats = new FrameStats(count, average, millis(median), millis(onePercent), millis(tenthPercent), millis(worst)); latest = stats; Log.info("frames: " + stats.describe() + " | total " + totalFrames); count = 0; windowStart = System.nanoTime(); } /** The frame time at that fraction of the window, in nanoseconds. */ private static double percentile(long[] sorted, double fraction) { if (sorted.length == 0) { return 0.0D; } int index = (int) (fraction * sorted.length); if (index >= sorted.length) { index = sorted.length - 1; } if (index < 0) { index = 0; } return sorted[index]; } private static double millis(double nanos) { return nanos / 1000000.0D; } /** The last window, never null. */ public static FrameStats latest() { return latest; } }