Improving timing and smoother script flow

You may request or share scripts for MicroMacro in this forum.
Post Reply
Message
Author
User avatar
Sirmabus
Posts: 7
Joined: Mon Sep 08, 2008 10:07 am

Improving timing and smoother script flow

#1 Post by Sirmabus » Sat Apr 04, 2009 6:18 am

In working with my own system I ran into some timing problems.
In particular I noticed some issues where something would work fine on fast machine, but then doing something different on slower machines.

Example: A short key press to move forward would work fine on my Core Duo, but then on an old slower Pentium 4 machine, the key press seemed to last much longer.


The core of the problem is the main sleep/rest routine
Here is yours AFAIK (current?):

Code: Select all

-- A pretty standard rest/sleep command.
-- It automatically will yield, though.
function yrest(msec)
  safeYield();

  if( msec < 10 ) then
    rest(msec);
    return;
  else
    sections = math.floor(msec / 100); -- split into 10msec sections
    ext = math.mod(msec, 100); -- any left overs...

    for b = 1,sections do
      rest(100);

      safeYield();
    end;

    if( ext > 0 ) then
      rest(ext);
    end
  end
end
If you think about it a bit, you will see that you can't guarantee this routine is going to take "msec" time.
What ever happens inside the coroutine might take longer then your smallest time tick division. Also inaccuracies do to other factors such as general OS
operation, what kind of priority threads are at, etc.
And since Windows is not a "Real-Time Operating System" (known problem) there is no guarantee how accurate a "Sleep()" (the core Windows API, call or the internal 100ns "NtDelayExecution()").

You might go to do a "yrest(75)" thinking you are getting a 75ms delay, when in truth after yielding to your system coroutine and the fact the damn game is taking near 100% CPU, the wait actually takes 250ms! (Just an example, could be better or worse)

How to at least alleviate this is to time how long you have been waiting over your series of time slices/ticks.


We have a bit different API but hopefully my routine here should make sense:

Code: Select all

--== Sleep with time sliced yield
local cSleepTick = 0.020
--
function CoreSystem:Sleep(Period)
    local StartTime = time.Get()

    -- Slice out the wait time with yield
    while true do

        -- Yield to core
        self:SafeYield()

        -- Need to track time elapsed to compensate for time spent in core, and in general improve accuracy.
        local TimeLeft = (Period - time.Delta(StartTime))
        if (TimeLeft >= 0.001) then
            -- Use a sleep tick or the remaining time, which ever is smaller
            if (TimeLeft > cSleepTick) then TimeLeft = cSleepTick end
		
	     -- Sleep a time slice
	     time.Sleep(TimeLeft)
      else
         return
      end      
    end
end
Example:

Code: Select all

-- Sleep for 1/4 of a second
tCoreSystem:Sleep(0.250)
Some differences in API I'm using all fractional seconds, not ms.
And time functions with 1ms accuracy (internally "timeGetTime()", with a "timeBeginPeriod(1)" on init).
(I switched to that rather then "QueryPerformanceFrequency()" since it takes several times more overhead per call.)

I save the the time before waiting then compensate, and, or bail out after every time slice.
You can see I also use a slice of 20ms. This seems to be pretty smooth, although as I write this you would think 10ms or smaller would be better..

I changed everywhere I did timing from a static time slice way to this way and every thing got smoother and less error prone..

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Improving timing and smoother script flow

#2 Post by Administrator » Sat Apr 04, 2009 12:01 pm

Thanks for the code. Your suggestions are always valuable to me, Sirmabus. I actually would like to add precision timing to MicroMacro not only for more accurate sleeping, but sub-second timing. I often use os.time() and os.difftime() to check how much time has passed between two points, but it would be nice to be able to do so on fractions of a second as well, which Lua does not currently support.

One problem I had noticed when working on that function is that certain increments would give problems. That is, ~10ms chunks would give errors about yielding across C-boundary or metamethods, ~50ms chunks would work for some people and not others, and ~100ms chunks worked perfectly fine for everybody. I honestly do not understand the logic behind that, as it's all the same code, just sleeping different amounts of time. Have you noticed any trouble like this?

Finally, I do not think that the problems with what you describe are (entirely) due to inaccuracies in Sleep. It typically would only account for a few ms difference. I think the core problem here is mostly to be blamed on Lua's coroutine system. If a section of code takes longer to execute than expected, it slows down all other threads. This gets to be a problem when people rarely use yrest() or coroutine.yield() in their scripts, causing it to spend more time processing their own script rather than checking for hotkeys and such. The only real thing that can be done about this would be to move all of that code into the core and run it in a separate thread, but this might cause problems. I've experimented with it before, but it caused all sorts of weird problems.


EDIT: It wasn't much code to add, so I went ahead and did it. Adding these new functions allows a user to profile code pretty easily, which resulted in me finding out that yrest() was taking much longer than expected (almost 50% longer!). I've modified yrest() to use the new timing functions for improved accuracy.

User avatar
Sirmabus
Posts: 7
Joined: Mon Sep 08, 2008 10:07 am

Re: Improving timing and smoother script flow

#3 Post by Sirmabus » Sun Apr 05, 2009 7:18 am

Thanks,

Well like I say there I use "timeGetTime()". And save and compare the results of each call internally to catch the roll over (if youre PC happens to be running for over ~49.71 days).

No, not actual errors anyhow. You get actual Lua errors, or just "error"/bad phenomena that you observe?
I use CoCo too as you know (internally, no option externally to disable it).

I agree, well it's a certain amount of assumption anyhow. I didn't dig into the internals to figure out the exact cause(s).
I did a lot of experiments though. Like just taking time stamps around various timer functions.
As I write this I think a good test might be to compare things both with, and with out CoCo.

I suppose in the end, it's just taking at least some account for time spent in the coroutine(s).
The above way, tracking the actual time spent, seem to fix it for the most part.

I suppose that's some of the oddness, or rather some sort of paradigm that that fits to get some sort of
multitasking in Lua to help manage things. Hopefully those people will read this :-P
Maybe you can write some little article about it.

For the longest time I didn't use coroutines, and just time sliced stuff out.
It worked ok, but it was certainly limited and harder to balance.
I couldn't have built my current setup (with many things happening at once) with out them.
But, still has to be time sliced of course.

I think we can add threads fairly easy by adding critical section object to "lua_lock()" and "lua_unlock()" et al.
But then I think that would add a lot of complexity. As I understand it, each thread would have to have it's own seperate Lua state, etc. If I really wanted multi-threading I'd probably think of using a different language..

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Improving timing and smoother script flow

#4 Post by Administrator » Sun Apr 05, 2009 6:54 pm

Sirmabus wrote:Thanks,

Well like I say there I use "timeGetTime()". And save and compare the results of each call internally to catch the roll over (if youre PC happens to be running for over ~49.71 days).
I used similar functions, but instead I instead decided to use a 64 bit integer for storing the values. I accomplished this by using a Lua table of a high part, and low part. See QueryPerformanceCounter(). This should prevent rollover from happening within any reasonable time.
No, not actual errors anyhow. You get actual Lua errors, or just "error"/bad phenomena that you observe?
I use CoCo too as you know (internally, no option externally to disable it).
Yes, LuaCoco does prevent it from happening. Without Coco, trying to sleep at 10ms increments causes a runtime error. It's very strange to me, but not too much of a problem.
[/quote]


I suppose 'critical' stuff (such as hotkeys and such) could be built-in to the interpreter and executed in a separate thread. This would alleviate some of the problem, but not entirely. How would you force Lua's execution to pause when you want it to? You can't, to my knowledge (without modifying Lua, anyways). So that means you'd still have to make use of coroutine.yield() to make Lua see that you want to 'pause' execution. Doesn't seem to be getting much done. And this would result in it not being modifiable by the end-user, either.

User avatar
Sirmabus
Posts: 7
Joined: Mon Sep 08, 2008 10:07 am

Re: Improving timing and smoother script flow

#5 Post by Sirmabus » Wed Apr 08, 2009 4:28 am

You got me thinking about timer resolution, so I did some math on it.

Also note at first I was using QPC ("QueryPerformanceCounter()") but I was concerned about what I read about that API call being "slow".
(I got obsessed about it, to where I tried to limit timer calls in my scripts :-/)
I did some timings on it, and in fact QPC was any times slower then TGT ("timeGetTime()"). You can run many TGT's in the time of one QPC call. I believe it's because QPC must go to the hardware (and thus a ring 0 call or two).
But it's all relative anyhow, on a modern machine would you notice a few micro second here and there? Probably not.
A plus side with QPC you get at least micro second accuracy too, while just 1ms at best with TGT.
At least now my obsession is over, and I feel free to use timers with reckless abandon (using TGT) :-P

Now as you probably run into some times with Lua. Doubles are great but you are limited to 52 bits of precision (see http://en.wikipedia.org/wiki/Double_precision).
There has been times when I was working on a game that used 64 ID values (some times for normal world objects, like WOW does, etc., and commonly for item IDs), and I thought they kept the values up only to 0x00FFFFFFFFFFFFFF, but didn't. Starts dropping low bit(s) off the ID and that won't work.
Like you say, the solution was to split them into a "high" and "low".

Anyhow I did a little calculations here.
52 bits is not a small number - 4,503,599,627,370,496
Now if you take the worst QPC case the frequency will probably be 3,579,545, although any time I looked at my own machines I got 1,193,000 (using "QueryPerformanceFrequency()"). And there is no guaranteed value.

Even at the faster freq that's:
4503599627370496 / 3579545 = 1258148627.094 seconds

At that rate over minutes, hours, days, even years, that's 39.9 years!
Suffice to say, it's probably safe for us to use a single double to store a QPC time value..

zer0
Posts: 213
Joined: Sat Feb 16, 2008 11:55 pm

Re: Improving timing and smoother script flow

#6 Post by zer0 » Mon Apr 20, 2009 2:42 am

I altered yrest to run at 25ms intervals, as running it in a virtual machine with 10ms was too inaccurate.

User avatar
Administrator
Site Admin
Posts: 5306
Joined: Sat Jan 05, 2008 4:21 pm

Re: Improving timing and smoother script flow

#7 Post by Administrator » Mon Apr 20, 2009 5:01 am

Was that before I made the change to high-res timers (you can check this by checking if getTime is nil)? It would make more sense that 10ms intervals would be more inaccurate in a virtual environment with the old system.

zer0
Posts: 213
Joined: Sat Feb 16, 2008 11:55 pm

Re: Improving timing and smoother script flow

#8 Post by zer0 » Tue Apr 21, 2009 8:37 am

Administrator wrote:Was that before I made the change to high-res timers (you can check this by checking if getTime is nil)? It would make more sense that 10ms intervals would be more inaccurate in a virtual environment with the old system.
Not sure, I believe I was using MM v0.99 so it probably would have been the old system.

Post Reply

Who is online

Users browsing this forum: No registered users and 6 guests