Skip to content

Commit

Permalink
Avoid stability problems
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Feb 13, 2024
1 parent e083062 commit 865d7f2
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 228 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ For demonstration purposes, this project also has a `testbench` [module
![](./update-monitor.png)

### Add as dependency
Since this library only has 4 small classes, you could just
Since this library only has 3 small classes, you could just
copy & paste them into your own project. If you prefer
proper dependency management, you can use Jitpack:

Expand All @@ -71,7 +71,7 @@ repositories {
...
dependencies {
...
implementation 'com.github.knokko.update-loop:implementation:v1.0.0'
implementation 'com.github.knokko.update-loop:implementation:v1.0.1'
}
```

Expand All @@ -89,6 +89,6 @@ dependencies {
<dependency>
<groupId>com.github.knokko.update-loop</groupId>
<artifactId>implementation</artifactId>
<version>v1.0.0</version>
<version>v1.0.1</version>
</dependency>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.knokko.update;

class Reference {

long time = System.nanoTime();
long counter;
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,70 @@

import java.util.function.Consumer;

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) {
SlidingEntry oldest = window.oldest();
if (oldest == null) return 0L;

long updateAt = oldest.value + (oldest.age + 1) * period;
long sleepNanoTime = updateAt - currentTime;

return sleepNanoTime / 1_000_000;
}

private final Consumer<UpdateLoop> updateFunction;
private final SlidingWindow slidingWindow;

private volatile long period;
private volatile long maximumBacklog;
private volatile Reference reference;
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.
* nanoseconds. It will allow a maximum backlog of {@code initialMaximumBacklog} nanoseconds. Any additional
* backlog will be discarded. The period and maximum backlog can be changed at any time.
* @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.
* @param initialMaximumBacklog The initial maximum backlog of the update function, in nanoseconds.
*/
public UpdateLoop(Consumer<UpdateLoop> updateFunction, long initialPeriod, int windowSize) {
public UpdateLoop(Consumer<UpdateLoop> updateFunction, long initialPeriod, long initialMaximumBacklog) {
if (updateFunction == null || initialPeriod < 0 || initialMaximumBacklog < 0) {
throw new IllegalArgumentException();
}
this.updateFunction = updateFunction;
this.slidingWindow = new SlidingWindow(windowSize);
this.period = initialPeriod;
this.maximumBacklog = initialMaximumBacklog;
}

/**
* 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.
* nanoseconds. It will use a default maximum backlog of 500 milliseconds.
* @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)));
this(updateFunction, initialPeriod, 500_000_000L);
}

/**
* 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>
* Changes the period of this update loop to {@code newPeriod} nanoseconds. Even though this class allows any
* non-negative period, periods below approximately 100 nanoseconds can't be achieved because the
* update loop overhead will become larger than the update 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();
if (newPeriod < 0) throw new IllegalArgumentException();
reference = new Reference();
period = newPeriod;
}

/**
* Changes the maximum backlog of this update loop to {@code newBacklog} nanoseconds.<br>
* <b>Thread safety</b>: this method can be called from any thread at any time.
* @param newBacklog The new maximum backlog, in nanoseconds
*/
public void setMaximumBacklog(long newBacklog) {
if (newBacklog < 0) throw new IllegalArgumentException();
maximumBacklog = newBacklog;
}

/**
* <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.
Expand All @@ -72,6 +74,14 @@ public long getPeriod() {
return period;
}

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

/**
* Starts this update loop on a new thread. This function must be called at most once.
*/
Expand All @@ -80,14 +90,24 @@ public void start() {
}

/**
* Stops this update loop. After invoking this method, the update function will be called at most once. Calling
* Stops this update loop. After invoking this method, the update function will be called at most once.
* If this method is called during the update function, the update function won't be invoked again. 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;
}

private long determineSleepTime(long currentTime) {
long nextUpdateAt = reference.time + reference.counter * period;
long nextSleepTime = nextUpdateAt - currentTime;

if (-nextSleepTime > maximumBacklog) reference.time += -nextSleepTime - maximumBacklog;

return nextSleepTime / 1000_000L;
}

/**
* 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.
Expand All @@ -96,12 +116,13 @@ public void stop() {
public void run() {
if (didStart) throw new IllegalStateException("This update loop has already started");
didStart = true;
reference = new Reference();

outerLoop:
while (shouldContinue) {
long sleepTime;
do {
sleepTime = determineSleepTime(slidingWindow, System.nanoTime(), period);
sleepTime = determineSleepTime(System.nanoTime());
if (sleepTime > 0L) {
try {
//noinspection BusyWait
Expand All @@ -112,8 +133,10 @@ public void run() {
}
} while (sleepTime > 0L);

slidingWindow.insert(System.nanoTime());
if (shouldContinue) updateFunction.accept(this);
if (shouldContinue) {
updateFunction.accept(this);
reference.counter += 1;
}
}
}
}

This file was deleted.

Loading

0 comments on commit 865d7f2

Please sign in to comment.