diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java b/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java index 9b986dca..7e0bf4c8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/IntervalledCounter.java @@ -30,40 +30,51 @@ package com.velocitypowered.proxy.util; *
This class is not thread-safe. If multiple threads access an instance concurrently, * external synchronization is required.
*/ -@SuppressWarnings("checkstyle:WhitespaceAfter") // Not our class public final class IntervalledCounter { private static final int INITIAL_SIZE = 8; + /** + * Data points within this many nanoseconds of the newest one are merged into it, bounding the + * number of stored data points to roughly {@code interval / COALESCE_INTERVAL}. + */ + private static final long COALESCE_INTERVAL = 1_000_000L; // 1ms + /** * Ring buffer holding the timestamp (in nanoseconds) for each data point. */ - protected long[] times; + private long[] times; + /** * Ring buffer holding the count associated with each timestamp. */ - protected long[] counts; + private long[] counts; + /** * The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval) * are considered part of the window. */ - protected final long interval; + private final long interval; + /** * Cached lower bound of the window (in nanoseconds) after the last update. */ - protected long minTime; + private long minTime; + /** * Running sum of all counts currently within the window. */ - protected long sum; + private long sum; + /** * Head index (inclusive) of the ring buffer. */ - protected int head; // inclusive + private int head; // inclusive + /** * Tail index (exclusive) of the ring buffer. */ - protected int tail; // exclusive + private int tail; // exclusive /** * Creates a new counter with the specified interval. @@ -131,6 +142,8 @@ public final class IntervalledCounter { /** * Adds {@code count} units at the specified timestamp, assuming the timestamp is within the * current window. If the timestamp is older than {@code minTime}, the value is ignored. + * If the timestamp is within {@link #COALESCE_INTERVAL} of the newest stored data point, the + * count is merged into that data point instead of creating a new one. * This method does not automatically advance the window; callers should invoke * {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand. * @@ -142,6 +155,15 @@ public final class IntervalledCounter { if (currTime - this.minTime < 0) { return; } + if (this.head != this.tail) { + final int last = this.tail == 0 ? this.times.length - 1 : this.tail - 1; + // guard against overflow by using subtraction + if (currTime - this.times[last] < COALESCE_INTERVAL) { + this.counts[last] += count; + this.sum += count; + return; + } + } int nextTail = (this.tail + 1) % this.times.length; if (nextTail == this.head) { this.resize(); @@ -219,7 +241,7 @@ public final class IntervalledCounter { * @return the rate in units per second for the current window */ public double getRate() { - return (double)this.sum / ((double)this.interval * 1.0E-9); + return (double) this.sum / ((double) this.interval * 1.0E-9); } /** diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java new file mode 100644 index 00000000..f187ff2c --- /dev/null +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/IntervalledCounterTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see