]> Cypherpunks repositories - gostls13.git/commitdiff
runtime, time: don't use monotonic clock inside synctest bubbles
authorDamien Neil <dneil@google.com>
Wed, 12 Mar 2025 23:00:26 +0000 (16:00 -0700)
committerGopher Robot <gobot@golang.org>
Tue, 18 Mar 2025 17:50:51 +0000 (10:50 -0700)
Don't include a monotonic time in time.Times created inside
a bubble, to avoid the confusion of different Times using
different monotonic clock epochs.

For #67434

goos: darwin
goarch: arm64
pkg: time
cpu: Apple M1 Pro
         │ /tmp/bench.0 │            /tmp/bench.1            │
         │    sec/op    │   sec/op     vs base               │
Since-10    18.42n ± 2%   18.68n ± 1%       ~ (p=0.101 n=10)
Until-10    18.28n ± 2%   18.46n ± 2%  +0.98% (p=0.009 n=10)
geomean     18.35n        18.57n       +1.20%

Change-Id: Iaf1b80d0a4df52139c5b80d4bde4410ef8a49f2f
Reviewed-on: https://go-review.googlesource.com/c/go/+/657415
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
src/internal/synctest/synctest_test.go
src/runtime/time.go
src/time/time.go
src/time/time_test.go

index 450d5f54166943ec2be181ce05cbfda07ba52364..62acb42359acff5ada64ee3596fdbaf3cb024605 100644 (file)
@@ -37,6 +37,27 @@ func TestNow(t *testing.T) {
        })
 }
 
+// TestMonotonicClock exercises comparing times from within a bubble
+// with ones from outside the bubble.
+func TestMonotonicClock(t *testing.T) {
+       start := time.Now()
+       synctest.Run(func() {
+               time.Sleep(time.Until(start.Round(0)))
+               if got, want := time.Now().In(time.UTC), start.In(time.UTC); !got.Equal(want) {
+                       t.Fatalf("time.Now() = %v, want %v", got, want)
+               }
+
+               wait := 1 * time.Second
+               time.Sleep(wait)
+               if got := time.Since(start); got != wait {
+                       t.Fatalf("time.Since(start) = %v, want %v", got, wait)
+               }
+               if got := time.Now().Sub(start); got != wait {
+                       t.Fatalf("time.Now().Sub(start) = %v, want %v", got, wait)
+               }
+       })
+}
+
 func TestRunEmpty(t *testing.T) {
        synctest.Run(func() {
        })
index c22d39c0894e35c0d4c8c383d38371cb28da71c0..3ece161cf47e5a6a880d068348a6dcb9a5dbc8b0 100644 (file)
@@ -18,7 +18,13 @@ func time_runtimeNow() (sec int64, nsec int32, mono int64) {
        if sg := getg().syncGroup; sg != nil {
                sec = sg.now / (1000 * 1000 * 1000)
                nsec = int32(sg.now % (1000 * 1000 * 1000))
-               return sec, nsec, sg.now
+               // Don't return a monotonic time inside a synctest bubble.
+               // If we return a monotonic time based on the fake clock,
+               // arithmetic on times created inside/outside bubbles is confusing.
+               // If we return a monotonic time based on the real monotonic clock,
+               // arithmetic on times created in the same bubble is confusing.
+               // Simplest is to omit the monotonic time within a bubble.
+               return sec, nsec, 0
        }
        return time_now()
 }
@@ -32,6 +38,11 @@ func time_runtimeNano() int64 {
        return nanotime()
 }
 
+//go:linkname time_runtimeIsBubbled time.runtimeIsBubbled
+func time_runtimeIsBubbled() bool {
+       return getg().syncGroup != nil
+}
+
 // A timer is a potentially repeating trigger for calling t.f(t.arg, t.seq).
 // Timers are allocated by client code, often as part of other data structures.
 // Each P has a heap of pointers to timers that it manages.
index 14e79672cad6f90891b62fcbbb5db0d3a1a2898c..bcaeee407ee9fec8cb1208698067612a5e0e2951 100644 (file)
@@ -1221,7 +1221,7 @@ func subMono(t, u int64) Duration {
 // Since returns the time elapsed since t.
 // It is shorthand for time.Now().Sub(t).
 func Since(t Time) Duration {
-       if t.wall&hasMonotonic != 0 {
+       if t.wall&hasMonotonic != 0 && !runtimeIsBubbled() {
                // Common case optimization: if t has monotonic time, then Sub will use only it.
                return subMono(runtimeNano()-startNano, t.ext)
        }
@@ -1231,7 +1231,7 @@ func Since(t Time) Duration {
 // Until returns the duration until t.
 // It is shorthand for t.Sub(time.Now()).
 func Until(t Time) Duration {
-       if t.wall&hasMonotonic != 0 {
+       if t.wall&hasMonotonic != 0 && !runtimeIsBubbled() {
                // Common case optimization: if t has monotonic time, then Sub will use only it.
                return subMono(t.ext, runtimeNano()-startNano)
        }
@@ -1325,6 +1325,9 @@ func runtimeNow() (sec int64, nsec int32, mono int64)
 //go:linkname runtimeNano
 func runtimeNano() int64
 
+//go:linkname runtimeIsBubbled
+func runtimeIsBubbled() bool
+
 // Monotonic times are reported as offsets from startNano.
 // We initialize startNano to runtimeNano() - 1 so that on systems where
 // monotonic time resolution is fairly low (e.g. Windows 2008
index ff253be46ba428ef8b37b6729238006b34f3670d..dcb477b65817461c2016cf41ec44116704327483 100644 (file)
@@ -1497,6 +1497,20 @@ func BenchmarkNowUnixMicro(b *testing.B) {
        }
 }
 
+func BenchmarkSince(b *testing.B) {
+       start := Now()
+       for b.Loop() {
+               u = int64(Since(start))
+       }
+}
+
+func BenchmarkUntil(b *testing.B) {
+       end := Now().Add(1 * Hour)
+       for b.Loop() {
+               u = int64(Until(end))
+       }
+}
+
 func BenchmarkFormat(b *testing.B) {
        t := Unix(1265346057, 0)
        for i := 0; i < b.N; i++ {