Skip to content

Commit

Permalink
Add doc comments
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Feb 9, 2024
1 parent 1072599 commit 4a8a786
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package com.github.knokko.update;

/**
* A utility class for measuring the number of updates per second (or some other time interval). Usage:
* <ul>
* <li>Create an instance of <i>UpdateCounter</i></li>
* <li>Call its <b>increment()</b> method at the start of every update</li>
* <li>Call its <b>getValue()</b> method to get the number of updates per period</li>
* </ul>
*/
public class UpdateCounter {

private final long period;

/**
* @param period The period of the counter. The number of updates per period will be counted.
*/
public UpdateCounter(long period) {
this.period = period;
}

/**
* Constructs a counter with a period of 1 second.
*/
public UpdateCounter() {
this(1_000_000_000L);
}
Expand All @@ -16,7 +30,7 @@ public UpdateCounter() {
private long counter = 0;
private volatile long value = -1;

public void increment(long currentTime) {
void increment(long currentTime) {
if (referenceTime == 0L) referenceTime = currentTime;

long passedTime = currentTime - referenceTime;
Expand All @@ -29,10 +43,21 @@ public void increment(long currentTime) {
counter += 1;
}

/**
* Increments the counter. This should be done at the start of every update (or frame).<br>
* <b>Thread safety</b>: This method must always be called on the same thread.
*/
public void increment() {
increment(System.nanoTime());
}

/**
* Gets the number of updates (calls to <b>increment()</b>) that happened during the last full period.
* If no full period has completed yet, it will return -1 instead.<br>
* <b>Thread safety</b>: This method can be called from any thread at any time.
* Multiple threads can safely call it at the same time,
* and even while another thread is calling <b>increment()</b>.
*/
public long getValue() {
return value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import static java.lang.Math.max;

/**
* An `UpdateLoop` tries to ensure that a given (update) function is executed periodically with a given period.
*/
public class UpdateLoop implements Runnable {

static long determineSleepTime(SlidingWindow window, long currentTime, long period) {
Expand All @@ -21,36 +24,79 @@ static long determineSleepTime(SlidingWindow window, long currentTime, long peri

private volatile long period;
private volatile boolean shouldContinue = true;

private volatile boolean didStart = false;

/**
* Constructs an <i>UpdateLoop</i> that attempts to execute {@code updateFunction} every {@code initialPeriod}
* nanoseconds. It will keep track of how many updates occurred the last {@code initialPeriod * windowSize}
* nanoseconds and use this information to maintain a stable update rate.
* @param updateFunction The function that should be called periodically. The parameter will always be this
* <i>UpdateLoop</i>, which is convenient for e.g. stopping it.
* @param initialPeriod The initial period of the update function, in nanoseconds.
* @param windowSize The number of past updates from which their timestamps will be remembered. This information
* is needed to achieve a robust update rate.
*/
public UpdateLoop(Consumer<UpdateLoop> updateFunction, long initialPeriod, int windowSize) {
this.updateFunction = updateFunction;
this.slidingWindow = new SlidingWindow(windowSize);
this.period = initialPeriod;
}

/**
* Constructs an <i>UpdateLoop</i> that attempts to execute {@code updateFunction} every {@code initialPeriod}
* nanoseconds. It will use a default sliding window of 1 second.
* @param updateFunction The function that should be called periodically
* @param initialPeriod The initial period of the update function, in nanoseconds
*/
public UpdateLoop(Consumer<UpdateLoop> updateFunction, long initialPeriod) {
this(updateFunction, initialPeriod, max(4, (int) (1_000_000_000L / initialPeriod)));
}

/**
* Changes the period of this update loop to {@code newPeriod} nanoseconds. Note that the performance of the
* update loop may degrade if the sliding window is too small. This would for instance happen if the sliding
* window size was determined automatically, and the new period is much smaller than the original period.<br>
* <b>Thread safety</b>: this method can be called from any thread at any time.
* @param newPeriod The new period, in nanoseconds
*/
public void setPeriod(long newPeriod) {
slidingWindow.forget();
period = newPeriod;
}

/**
* <b>Thread safety</b>: this method can be called from any thread at any time.
* @return The current period of this update loop, in nanoseconds.
*/
public long getPeriod() {
return period;
}

/**
* Starts this update loop on a new thread. This function must be called at most once.
*/
public void start() {
new Thread(this).start();
}

/**
* Stops this update loop. After invoking this method, the update function will be called at most once. Calling
* this method more than once has the same effect as calling it once.
* <b>Thread safety</b>: this method can be called from any thread at any time.
*/
public void stop() {
shouldContinue = false;
}

/**
* Runs this update loop on the current thread. It won't return until the update loop is finished.
* This method must be called at most once.
*/
@Override
public void run() {
if (didStart) throw new IllegalStateException("This update loop has already started");
didStart = true;

outerLoop:
while (shouldContinue) {
long sleepTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@

import static com.github.knokko.update.UpdateLoop.determineSleepTime;
import static java.lang.Math.abs;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

public class TestUpdateLoop {

Expand Down Expand Up @@ -102,4 +101,18 @@ public void testWithDynamicPeriod() throws InterruptedException {

if (abs(finalValue - expectedValue) > 3) assertEquals(expectedValue, finalValue);
}

@Test
public void testCannotStartTwice() throws InterruptedException {
UpdateLoop updateLoop = new UpdateLoop(loop -> {}, 100_000_000L);
updateLoop.start();

// Give the update thread time to start
Thread.sleep(500);

assertThrows(IllegalStateException.class, updateLoop::run);
updateLoop.stop();

assertThrows(IllegalStateException.class, updateLoop::run);
}
}

0 comments on commit 4a8a786

Please sign in to comment.