Skip to content

time: macOS monotonic time paused during sleep #66870

@Plasmatium

Description

@Plasmatium

Go version

go version go1.22.1 darwin/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''


GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''


GONOSUMDB=''
GOOS='darwin'

GOPRIVATE=''
GOPROXY='https://goproxy.cn,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.22.1'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/2x/l_3w120j35sgp9mpf4zlcjb40000gr/T/go-build2833465807=/tmp/go-build -gno-record-gcc-switches -fno-common'

What did you do?

The issue happens in both go run main.go and vscode debug mode, on my macbook.
The code is running as expected on a Linux server.
Maybe this is an OS bug
No unsafe code.

Two variables:
expiresAt: a time.Time variable that stores the token when to expire, created like time.Now().After(expiresIn)
now: a time.Time variable that stores token checking time.

Then I use now.Before(expiresAt) to decide if I should renew the token.

What did you see happen?

now is after expiresAt, means token already expired, but now.Before(expiresAt) returns true. Below is a screenshot in vscode debug console:

IMG_6613

Both now and expiresAt are created in the same process, and hasMonotonic bit is set to 1. So they just compare by ext:

time.Time defines here:

go/src/time/time.go

Lines 135 to 156 in d6c972a

type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}

Time.Before defines here:

go/src/time/time.go

Lines 265 to 273 in d6c972a

// Before reports whether the time instant t is before u.
func (t Time) Before(u Time) bool {
if t.wall&u.wall&hasMonotonic != 0 {
return t.ext < u.ext
}
ts := t.sec()
us := u.sec()
return ts < us || ts == us && t.nsec() < u.nsec()
}

From the screenshot we can see there is a monotonic clock rollback issue, the m printed is not as expected. now is after expiresAt, but its m is smaller. This is a breakpoint hit in debug mode(after I send an API call), two variables surely created in the same process.

The issue is not always happen, unless I close the screen and a night passed.
Seems that while the computer sleep and awake, it will reset the monotonic clock to zero.

I wrote some code to reproduce quickly, but failed. The monotonic clock only stopped, but not reset, while the computer sleep for two hours when I go out for a launch.

"Monotonic clock may stop" is mentioned here in doc monotonic clock, but seems like it won't rollback (if it can, it's not monotonic)

The failed reproduce code is written as here in playground

And the output:
image
We can see the monotonic clock is only just stopped, but not reset (no rollback). I'll keep testing to let it pass a whole night to see if it could be reproduced.

Besides, more info:
The value of monotonic clock (stored as Time.ext) eventually comes from here [nanotime1] on mac os:

func nanotime1() int64 {
var r struct {
t int64 // raw timer
numer, denom uint32 // conversion factors. nanoseconds = t * numer / denom.
}
libcCall(unsafe.Pointer(abi.FuncPCABI0(nanotime_trampoline)), unsafe.Pointer(&r))
// Note: Apple seems unconcerned about overflow here. See
// https://developer.apple.com/library/content/qa/qa1398/_index.html
// Note also, numer == denom == 1 is common.
t := r.t
if r.numer != 1 {
t *= int64(r.numer)
}
if r.denom != 1 {
t /= int64(r.denom)
}
return t
}
func nanotime_trampoline()

What did you expect to see?

I expect to see now.Before(expiresAt) should return false, then token can renew as expected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DocumentationIssues describing a change to documentation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.OS-Darwin

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions