Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Incorrect time calculation in loop.call_at() #233

Closed
yope opened this issue Mar 14, 2019 · 10 comments
Closed

Incorrect time calculation in loop.call_at() #233

yope opened this issue Mar 14, 2019 · 10 comments

Comments

@yope
Copy link

yope commented Mar 14, 2019

  • uvloop version: 0.11.2
  • Python version: 3.6.7
  • Platform: ubuntu 18.10
  • Can you reproduce the bug with PYTHONASYNCIODEBUG in env?: yes

There is a conceptual bug in the calculation of the time in call_at() here:
https://github.com/MagicStack/uvloop/blob/master/uvloop/loop.pyx#L1270
If an application calculates a time in the future to call a function with call_at(), it should be able to expect
the value of loop.time() when called inside that function to be equal or higher (later) than the timestamp that was specified. Due to timer resolution and the fact that call_at() internally does "when - self.time()" causes that this assumption is not guaranteed to be true.
The following script demonstrates this:

#!/usr/bin/env python3

import sys
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

def mytimer():
        global ti
        global loop
        ti0 = loop.time()
        print(ti, ti0)
        if ti > ti0:
                print("ERROR!!!")
                sys.exit(1)
        ti = ti0 + 0.1
        loop.call_at(ti, mytimer)


if __name__ == "__main__":
        global ti
        global loop
        loop = asyncio.get_event_loop()
        ti = loop.time()
        loop.call_at(ti + 1.0, mytimer)
        loop.run_forever()

Eventually this script will stop after printing "ERROR!!!". This means that the timer handler mytimer() was
called a bit too early.

@fantix
Copy link
Member

fantix commented Mar 16, 2019

I can reproduce this, even after fixing a floating-point precision issue in your demo script:

-        if ti > ti0:
+        if ti - ti0 > 0.0000001:

Also when failing, it is always called 1ms earlier than expected in my ~20 tries. I think the reason is:

  1. libuv only provides integers in millisecond with uv_now() which is used by uvloop.
  2. uvloop turns this millisecond to seconds in float.
  3. After tweaking (+ 0.1), the float second is again changed back to integer millisecond truncating all decimals e.g. 0.09899999998742715 * 1000 -> 98, hence losing the 1ms.

I think the fix would be as easy as using round instead of the implicit floor:

-        when = <uint64_t>(delay * 1000)
+        when = <uint64_t>round(delay * 1000)

At least it didn't report an error in 10+ minutes in my local testing.

@yope
Copy link
Author

yope commented Mar 16, 2019

@fantix Yes, I think you are right. It definitely is a rounding issue. Is this the only place that time is converted from float to milliseconds integer? I guess all such places would have to be changed...
For now, as a workaround in my code, I simply add 1ms to each timestamp given to call_at(). This seems to work well enough for me while waiting for the correct fix.

@fantix
Copy link
Member

fantix commented Mar 16, 2019

Is this the only place that time is converted from float to milliseconds integer?

Yes, this is the only place.

For now, as a workaround in my code, I simply add 1ms to each timestamp given to call_at().

Right, that should work for now. I'll create a PR to add the round if no objections.

@1st1
Copy link
Member

1st1 commented Mar 16, 2019

Yeah, go for it!

@fantix fantix added the bug label Mar 17, 2019
fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
@fantix fantix mentioned this issue Mar 17, 2019
2 tasks
fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
@jlaine
Copy link
Contributor

jlaine commented Mar 17, 2019

If we want this to be "no earlier than" shouldn't this be a ceil() ?

@fantix
Copy link
Member

fantix commented Mar 17, 2019

Using ceil() has a side-effect:

round((123123.1 + 0.1) * 1000)      # 123123200
math.ceil((123123.1 + 0.1) * 1000)  # 123123201

But yeah, I got your point that when called with loop.call_later(0.0004, ...), it may still get called too early. One possible solution could be to still use round(), but adding a time check before calling, and delay one more millisecond if the check fails. (This would also help with the failing unit test in this PR when PYTHONASYNCIODEBUG=1) However this would add a bit more complexity to the implementation, I'm not sure if it is worth it. Any thoughts, please?

@yope
Copy link
Author

yope commented Mar 17, 2019

UVloop has a timer resolution of 1ms, so if call_at of higher than that resolution is made, uvloop may round up or down to whole milliseconds. How it does that is not so important.
What is important, is that whenever I want to call_at() a time that is an integer number of milliseconds, it does not get called at an earlier integer number of milliseconds.
In other words, if I run call_at(12.34, func) it does not get called at 12.399. In this case, round() should suffice.
OTOH, if I call_at(12.3456, func) and func gets called at either 12.345 or 12.236, that is not an issue because this is due to the resolution of uvloop. If you need more resolution than that, don't use uvloop.

@1st1
Copy link
Member

1st1 commented Mar 17, 2019

when called with loop.call_later(0.0004, ...), it may still get called too early.

And that's probably fine, because the interval is less than the timer resolution. I think using round() instead of the implicit floor/cast should be a good enough solution.

@jlaine
Copy link
Contributor

jlaine commented Mar 17, 2019

Okidoki, I'll butt out ;)

fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
fantix added a commit to fantix/uvloop that referenced this issue Mar 17, 2019
fantix added a commit to fantix/uvloop that referenced this issue Mar 18, 2019
@fantix fantix added enhancement and removed bug labels Mar 18, 2019
@1st1 1st1 closed this as completed in #236 Mar 18, 2019
1st1 pushed a commit that referenced this issue Mar 18, 2019
1st1 pushed a commit that referenced this issue Mar 20, 2019
@1st1
Copy link
Member

1st1 commented Mar 20, 2019

Fixed in uvloop 0.12.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants