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

runtime: Timer buckets may get "stuck" for long periods of time after Windows 8/10 systems wake from sleep #31528

Open
jmontgomery-jc opened this issue Apr 17, 2019 · 4 comments

Comments

Projects
None yet
3 participants
@jmontgomery-jc
Copy link

commented Apr 17, 2019

What version of Go are you using (go version)?

PS C:\> go version
go version go1.12.4 windows/amd64

Does this issue reproduce with the latest release?

Yes, and also with go 1.11

What operating system and processor architecture are you using (go env)?

go env Output
PS C:\> go env
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\jordan\AppData\Local\go-build
set GOEXE=.exe
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOOS=windows
set GOPATH=c:\gopath
set GOPROXY=
set GORACE=
set GOROOT=C:\projects\go1.12.4.windows-amd64\go
set GOTMPDIR=
set GOTOOLDIR=C:\projects\go1.12.4.windows-amd64\go\pkg\tool\windows_amd64
set GCCGO=gccgo
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=
set CGO_CFLAGS=-g -O2
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-g -O2
set CGO_FFLAGS=-g -O2
set CGO_LDFLAGS=-g -O2
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=C:\Users\jordan\AppData\Local\Temp\go-build618130038=/tmp/go-build -gno-record-gcc-switches

What did you do?

I ran this program on a Windows 10 amd64 VM: https://play.golang.org/p/Jyqki-iIF54. I put the Windows VM to sleep(right click Start button->"Shut down or sign out"->"Sleep") right after seeing the first “ShortInterval 0 fired at…” print at 15:19:09. I then woke the machine up about 8 minutes later around 15:27:28 and the program continued. I later killed the program with ctrl+c around 15:34:30. Full program output copied below:

PS C:\> .\timertest.exe
doStuffLongInterval 2 starting
doStuffLongInterval 0 starting
doStuffShortInterval 0 starting
doStuffLongInterval 1 starting
ShortInterval 0 fired at 2019-04-16 15:19:09.159805 -0700 PDT m=+30.006215401 after 30 seconds
LongInterval 0 fired at 2019-04-16 15:27:28.4460785 -0700 PDT m=+529.330931901 after 5 minutes
ShortInterval 0 fired at 2019-04-16 15:27:28.4468785 -0700 PDT m=+529.331733901 after 30 seconds
LongInterval 2 fired at 2019-04-16 15:31:28.2059465 -0700 PDT m=+769.327050901 after 5 minutes
ShortInterval 0 fired at 2019-04-16 15:31:28.2059465 -0700 PDT m=+769.327050901 after 30 seconds
LongInterval 1 fired at 2019-04-16 15:31:28.2059465 -0700 PDT m=+769.327050901 after 5 minutes
ShortInterval 0 fired at 2019-04-16 15:31:58.205199 -0700 PDT m=+799.330143301 after 30 seconds
ShortInterval 0 fired at 2019-04-16 15:32:28.2045636 -0700 PDT m=+829.332021501 after 30 seconds
LongInterval 0 fired at 2019-04-16 15:32:28.2053959 -0700 PDT m=+829.332853801 after 5 minutes
ShortInterval 0 fired at 2019-04-16 15:32:58.2042094 -0700 PDT m=+859.333011301 after 30 seconds
ShortInterval 0 fired at 2019-04-16 15:33:28.2022148 -0700 PDT m=+889.334911201 after 30 seconds
ShortInterval 0 fired at 2019-04-16 15:33:58.1962279 -0700 PDT m=+919.335067001 after 30 seconds
ShortInterval 0 fired at 2019-04-16 15:34:28.1926601 -0700 PDT m=+949.335907901 after 30 seconds

What did you expect to see?

I would expect that while the system is awake the ShortInterval function would regularly print approximately every 30 seconds. I would also expect the LogInterval prints to be consistently placed in time rather than “LongInterval 0 …” seemingly being staggered and firing at different times than “LongInterval 1 …” and “LongInterval 2 …” which started at approximately the same time.

What did you see instead?

After waking up the system the “ShortInterval 0 fired” message was printed once and then was not printed again for 4 minutes, meaning its 30 second timer fired about 3.5 minutes too late.

I believe this is due to the design of the timerBucket mechanism within the Go runtime. Each timerBucket is sorted such that the timer with the lowest firing time(the “when” field on the runtime.timer struct) is always at the front and the goroutines that handle firing of timers sleep until they are:

  • Awakened from sleep at the time to fire the timer

  • Awakened by a new timer being placed at the front of the timerBucket which causes the sleep time to be recalculated.

On Windows, the current time is retrieved using nanotime() which grabs the time from the KUSER_SHARED_DATA structure and which continues “ticking” even while the system is asleep. I’ve tested and observed this behavior both in a VMWare VM running Windows 10 and on a laptop running Windows 10. Relevant code here: https://golang.org/src/runtime/sys_windows_amd64.s?h=nanotime#L468

The timed wakeup, on the other hand, is implemented in semasleep() using WaitForSingleObject which the documentation states does not count time spent in a “low-power state”(i.e. asleep) on Windows 8 and 10. See here: https://golang.org/src/runtime/os_windows.go?h=semasleep#L600 and here: https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitforsingleobject

What seems to happen here is that when a Windows 8 or 10 machine goes to sleep, the actual time returned by nanotime() can potentially surpass the “when” time of the timer at the front of the queue while the machine is asleep. Newly added timers will thus have a later “when” time and so will get queued behind the element at the front of the timerBucket and since WaitForSingleObject does not count this sleep time the wakeup will not come until the machine has actually been awake for the total amount of time originally specified for the timer, potentially making the new timers added to the timerBucket late by however long the original timer was for. All of this is invisible to a normal go program so from the user or programmer’s perspective, some timers simply stop firing.

In my testing I’ve noticed that this happens more often when machines have a low number of cores, due to the lower number of timerBuckets so most of my testing was done on a 2-core VM. That said, it does reproduce on real hardware if the number of “long interval” goroutines is increased.

@bradfitz

This comment has been minimized.

Copy link
Member

commented Apr 17, 2019

@ianlancetaylor

This comment has been minimized.

Copy link
Contributor

commented Apr 17, 2019

The analysis sounds entirely plausible to me. Unix systems avoid this problem by using the same clock for time and for sleeping. Does Windows have an option to do that?

@jmontgomery-jc

This comment has been minimized.

Copy link
Author

commented Apr 18, 2019

I don't know of any APIs on Windows that provide similar functionality. It appears that outside of the KUSER_SHARED_DATA method of getting timestamps there's also QPC(which the runtime uses for binaries running under Wine) but even that "keeps ticking" while the system is asleep: https://docs.microsoft.com/en-us/windows/desktop/sysinfo/acquiring-high-resolution-time-stamps

@jmontgomery-jc

This comment has been minimized.

Copy link
Author

commented Apr 25, 2019

It turns out I was wrong and there is a Windows API that provides a monotonic clock that does not "keep ticking" while the system is asleep:
https://docs.microsoft.com/en-us/windows/desktop/api/realtimeapiset/nf-realtimeapiset-queryunbiasedinterrupttime

The only downside of using this API is that it is unavailable on anything lower than Windows 7/Server 2008 R2. We may also see early wakeups on Win7/2k8R2 due to the differences highlighted above in WaitForSingleObject timeouts on these versions. Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.