[SOLVED] SimEthereal ZoneManager: configurable underflow and overflow rate

Hi

I have set up the GameLoop to run with 30 FPS (running with 60 FPS consume a noticeable amount of CPU as mentioned in the Javadoc).

Doing so, now console is filled with “zone update underflow” warning from ZoneManager:

This makes it hard to trace the logging output from other systems. I know I can disable the ZoneManager logging from log4j settings but I think it would be better to make underflow and overflow rate to be configurable by the user rather than being hardcoded.

Any thoughts?

Yeah, they should probably be configurable for exactly your use-case. Makes me wonder what other hidden 60 FPS assumptions might be in the networking.

Also, if your physics is taxing your CPU, you could also maybe skip every other frame so that the game loop could still run at 60. Of course, if it’s “all the systems” then 30 will have to work.

Yes, it’s about limiting all the systems to 30 FPS.

In case you like, I can make a PR to add setters and getters for configuring underflow and overflow frame rates.

1 Like

Thanks

1 Like

Are you Ok with this?

/**
 *   Sets fps limits which are used to print warnings when ZoneManager update rate
 *   goes under or over the specified value.
 *   By default underflow limit is 60 and overflow limit is 70.
 */
public void setFrameRateLimits( int frameUnderflowLimit, int frameOverflowLimit ) {
    this.frameUnderflowLimit = frameUnderflowLimit;
    this.frameOverflowLimit = frameOverflowLimit;
}

public int getFrameUnderflowLimit() {
    return frameUnderflowLimit;
}

public int getFrameOverflowLimit() {
    return frameOverflowLimit;
}

For bean-ness, probably should have separate setters. I’m ok with one combined one but probably at least need separate ones, too.

though it’s not a critical thing either way.

Edit: though if you added some sanity checking to the setter I’d be ok with just the one… ie: making sure the underflow is not larger than the overflow, etc.

1 Like

Ok, will add them.

Should this be fine now?

 /**
 *  Sets frame update limits which are used to print warnings when ZoneManager update rate
 *  goes under or over the specified value.
 *  By default underflow limit is 60 and overflow limit is 70.
 *
 *  @throws IllegalArgumentException if values are negative or underflow limit is larger than the overflow limit.
 */
public void setFrameRateLimits( int frameUnderflowLimit, int frameOverflowLimit ) {
    if(frameUnderflowLimit < 0 || frameOverflowLimit < 0) {
        throw new IllegalArgumentException("Frame limits must be positive.");
    }
    if(frameUnderflowLimit > frameOverflowLimit) {
        throw new IllegalArgumentException("Frame underflow limit must be less than the overflow limit.");
    }

    this.frameUnderflowLimit = frameUnderflowLimit;
    this.frameOverflowLimit = frameOverflowLimit;
}

/**
 *  Sets frame underflow limit. ZoneManager will print warnings if frame update
 *  rate goes below this limit. By default underflow limit is 60.
 *
 *  @throws IllegalArgumentException if value is negative or larger than the overflow limit.
 */
public void setFrameUnderflowLimit( int frameUnderflowLimit ) {
    if(frameUnderflowLimit < 0) {
        throw new IllegalArgumentException("Frame underflow limit must be positive.");
    }
    if(frameUnderflowLimit > frameOverflowLimit) {
        throw new IllegalArgumentException("Frame underflow limit must be less than the overflow limit.");
    }

    this.frameUnderflowLimit = frameUnderflowLimit;
}

public int getFrameUnderflowLimit() {
    return frameUnderflowLimit;
}

/**
 *  Sets frame overflow limit. ZoneManager will print warnings if frame update
 *  rate goes above this limit. By default overflow limit is 70.
 *
 *  @throws IllegalArgumentException if value is negative or less than the underflow limit.
 */
public void setFrameOverflowLimit( int frameOverflowLimit ) {
    if(frameOverflowLimit < 0) {
        throw new IllegalArgumentException("Frame overflow limit must be positive.");
    }
    if(frameOverflowLimit < frameUnderflowLimit) {
        throw new IllegalArgumentException("Frame overflow limit must be larger than the underflow limit.");
    }

    this.frameOverflowLimit = frameOverflowLimit;
}

public int getFrameOverflowLimit() {
    return frameOverflowLimit;
}

I am not commenting on the content but rather the fact that exceptions are thrown but the documentation for the method does not state that.

@throws

1 Like

Ah, yes. Going to add it as well.
Thanks

or maybe

@exception IllegalArgumentException if frame overflow limit is not positive.
@exception IllegalArgumentException if frame overflow limit is smaller than the underflow limit.
etc?

Ok, updated my above post to include @throws as well.

Looks good to me.

I guess it’s not about the systems taxing the CPU but about the way SiO2(?) does it’s update loop throttling.

When in 60 FPS Mode, you busy wait the CPU, maxing out the core, at least on Windows, I think.
In 30 FPS Mode, I think you use Thread.sleep() or anything else to yield the CPU, so the scheduler can do other things.

Here the consequence is people using the 30 FPS mode soley for that. What would you think about making this behavior toggleable? I think when Thread.sleep() is too slow, then an underflow happens, right? So the hosts can see if their environment exposes this problem.

I know for instance, that the OpenJDK uses a different Timer Resolution depending on the OS Version (Vista and up (?)) and the available API.

1 Like

The behavior of sleep is already configurable but I guess you are suggesting that the delta time is greater than half the update rate.

I suppose it’s a bit arbitrary but when update rate is 60 hz, the sleep() resolution on many OS’s will be at or greater than a 60 hz frame (or maybe that’s legacy vestigial lore on my part, it could be 10ms or 13ms ‘back in the day’). Definitely more than half a frame in that case.

PLUS, the delta is measured as actual loop to loop time… so if we skipped the sleep and the actual frame time is still more than half the desired time, there is no way we’d make it to the next frame on time if we slept.

sleep(0) is a special case that should only return immediately if there are no other higher priority threads waiting. So it may LOOK like your CPU is pegged but the CPU is still available to do other things.

sleep(1) is guaranteed to release the CPU frame… so you can set idleSleepTime to 1 and you will occasionally miss frames. (If the case you indicate is true then you will sleep every other frame and run every other frame a little late.)

The case I’m trying to prevent is getting underflow when CPU is 0… which is what was happening all. the. time. until I added this code.

It’s possible that I should swap this all out with LockSupport.parkNanos() which is supposedly more accurate than sleep.

It’s also possible that this should be a strategy object that the user can override.

Seems I am running to an issue applying the commit. Surprisingly this is just happening with SimEthereal project. Thought someone might have a clue what is going wrong here.

Seems it is committing the whole file rather than just committing the added/modified lines. Not sure why. I already tried it in two different IDEs but no difference.

Here is the commit log using Intellig IDE:

23:48:32.936: [SimEthereal] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false rm --ignore-unmatch --cached -r -- .idea/compiler.xml
23:52:45.961: [SimEthereal] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false add --ignore-errors -A -f -- src/main/java/com/simsilica/ethereal/zone/ZoneManager.java
23:52:46.018: [SimEthereal] git -c credential.helper= -c core.quotepath=false -c log.showSignature=false commit -F /tmp/git-commit-msg-.txt --
[master 8a6b271] ZoneManager:makes frame rate underflow/overflow limit configurable.
 1 file changed, 1000 insertions(+), 935 deletions(-)

And this is how it looks like on Github:

As said it is only happening with this project, I do not have this problem on other projects.

Anyone knows what may cause this or how I may solve it?

PS, if it is possible to somehow do only pick the changed lines upon merging, I can open a PR with the above commit if you are Ok with it.

Could be a line ending issue. Not sure what I setup different about this project that would cause it.

2 Likes

Hm… Yes, you were right. I disabled git autocrlf and it works fine now. Thank you.

git config --global core.autocrlf false

PR is submitted:

1 Like

Hahah… you can tell I only intended that code to be temporary because it wasn’t indented. :slight_smile:

Worth keeping now that the check is configurable because it’s saved my butt on a few occasions.

1 Like

Regarding this, I’ve just updated GameLoop to support a user-settable LoopSleepStrategy object. The default behavior is the existing one because in Windows, I suspect you will not do better than that. The old behavior is implemented by the LegacyLoopSleepStrategy.

There is also a new NanoLoopSleepStrategy that can be set that will use LockSupport.parkNanos(). Based on my reading online, this will be horrible on Windows 10 unless you are already doing something else that lowers the timer resolution. Else, LockSupport.parkNanos() cannot sleep for less than 16 ms!!!

For background: https://dzone.com/articles/locksupportparknanos-under-the-hood-and-the-curiou

These are the issues that I’m trying to work around from many years of hard-earned lessons with respect to Java timing on Windows… since back in the days I was implementing my own MIDI sequencers, etc… Accurately timed loops suck on Windows.

That being said, folks can try the NanoLoopSleepStrategy (or simply implement their own that always Thread.sleep(1) if they like).

Note that on my Windows 7 box, using one or the other didn’t seem to matter, either for loop accuracy or for CPU usage.

A nice side-effect of this refactoring is that one could also implement a wrapper strategy that kept track of how often the loop is late, etc.

1 Like