Compare commits

...

3 Commits

Author SHA1 Message Date
Josh Bleecher Snyder
197edcedec flex 2021-07-19 16:02:49 -07:00
Josh Bleecher Snyder
2a8ca8a592 switch from monotonic coarse to monotonic 2021-07-19 15:45:49 -07:00
Josh Bleecher Snyder
a43f939828 tstime: add MonotonicCoarse
MonotonicCoarse provides a coarse monotonic time.
On some platforms, it is implemented in assembly,
which lets us do much less work than time.Now,
which gets a high precision monotonic time and
a high precision wall time.

The assembly code is tied to a particular Go release
because it reaches into the Go internals
in order to switch to the system stack for the vdso call.

On my darwin/arm64 machine, there is no perf difference.
On my linux/amd64 machine, MonotonicCoarse is 5x faster (50ns -> 10ns).
On my linux/arm64 VM, MonotonicCoarse is 16x faster (64ns -> 4ns).

We could also use this in the rate limiter and magicsock,
which are two other uses of time.Now that show up in the CPU pprof
when doing throughput benchmarking.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-07-19 15:33:51 -07:00
6 changed files with 342 additions and 3 deletions

View File

@@ -18,6 +18,7 @@ import (
"golang.zx2c4.com/wireguard/tun"
"inet.af/netaddr"
"tailscale.com/net/packet"
"tailscale.com/tstime"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/wgengine/filter"
@@ -363,7 +364,7 @@ func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
// noteActivity records that there was a read or write at the current time.
func (t *Wrapper) noteActivity() {
atomic.StoreInt64(&t.lastActivityAtomic, time.Now().Unix())
atomic.StoreInt64(&t.lastActivityAtomic, tstime.MonotonicCoarse())
}
// IdleDuration reports how long it's been since the last read or write to this device.
@@ -371,8 +372,8 @@ func (t *Wrapper) noteActivity() {
// Its value is only accurate to roughly second granularity.
// If there's never been activity, the duration is since 1970.
func (t *Wrapper) IdleDuration() time.Duration {
sec := atomic.LoadInt64(&t.lastActivityAtomic)
return time.Since(time.Unix(sec, 0))
elapsed := tstime.MonotonicCoarse() - atomic.LoadInt64(&t.lastActivityAtomic)
return time.Duration(elapsed) * time.Second
}
func (t *Wrapper) Read(buf []byte, offset int) (int, error) {

34
tstime/monocoarse_asm.go Normal file
View File

@@ -0,0 +1,34 @@
// +build go1.16,!go1.17
// +build linux,amd64 linux,arm64
package tstime
const (
CLOCK_MONOTONIC = 1
CLOCK_MONOTONIC_COARSE = 6
)
// MonotonicCoarse returns the number of monotonic seconds elapsed
// since an unspecified starting point, at low precision.
// It is only meaningful when compared to the
// result of previous MonotonicCoarse calls.
// On some platforms, MonotonicCoarse is much faster than time.Now.
func monoClock(clock int) int64
// Monotonic returns the number of monotonic seconds elapsed
// since an unspecified starting point, at low precision.
// It is only meaningful when compared to the
// result of previous Monotonic calls.
// On some platforms, Monotonic is much faster than time.Now.
func Monotonic() int64 {
return monoClock(CLOCK_MONOTONIC)
}
// MonotonicCoarse returns the number of monotonic seconds elapsed
// since an unspecified starting point, at low precision.
// It is only meaningful when compared to the
// result of previous MonotonicCoarse calls.
// On some platforms, MonotonicCoarse is much faster than time.Now.
func MonotonicCoarse() int64 {
return monoClock(CLOCK_MONOTONIC_COARSE)
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Adapted from code in the Go runtime package at Go 1.16.6:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.16
// +build !go1.17
#include "go_asm.h"
#include "textflag.h"
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#define SYS_clock_gettime 228
// Hard-coded offsets into runtime structs.
// Generated by adding the following code
// to package runtime and then executing
// and empty func main:
//
// func init() {
// println("#define g_m", unsafe.Offsetof(g{}.m))
// println("#define g_sched", unsafe.Offsetof(g{}.sched))
// println("#define gobuf_sp", unsafe.Offsetof(g{}.sched.sp))
// println("#define m_g0", unsafe.Offsetof(m{}.g0))
// println("#define m_curg", unsafe.Offsetof(m{}.curg))
// println("#define m_vdsoSP", unsafe.Offsetof(m{}.vdsoSP))
// println("#define m_vdsoPC", unsafe.Offsetof(m{}.vdsoPC))
// }
#define g_m 48
#define g_sched 56
#define gobuf_sp 0
#define m_g0 0
#define m_curg 192
#define m_vdsoSP 832
#define m_vdsoPC 840
// func monoClock(clock int) int64
TEXT ·monoClock(SB),NOSPLIT,$16-16
MOVQ clock+0(FP), DI
// Switch to g0 stack.
MOVQ SP, R12 // Save old SP; R12 unchanged by C code.
get_tls(CX)
MOVQ g(CX), AX
MOVQ g_m(AX), BX // BX unchanged by C code.
// Set vdsoPC and vdsoSP for SIGPROF traceback.
// Save the old values on stack and restore them on exit,
// so this function is reentrant.
MOVQ m_vdsoPC(BX), CX
MOVQ m_vdsoSP(BX), DX
MOVQ CX, 0(SP)
MOVQ DX, 8(SP)
LEAQ ret+8(FP), DX
MOVQ -8(DX), CX
MOVQ CX, m_vdsoPC(BX)
MOVQ DX, m_vdsoSP(BX)
CMPQ AX, m_curg(BX) // Only switch if on curg.
JNE noswitch
MOVQ m_g0(BX), DX
MOVQ (g_sched+gobuf_sp)(DX), SP // Set SP to g0 stack
noswitch:
SUBQ $16, SP // Space for results
ANDQ $~15, SP // Align for C code
LEAQ 0(SP), SI
MOVQ runtime·vdsoClockgettimeSym(SB), AX
CMPQ AX, $0
JEQ fallback
CALL AX
ret:
MOVQ 0(SP), AX // sec
MOVQ 8(SP), DX // nsec
MOVQ R12, SP // Restore real SP
// Restore vdsoPC, vdsoSP
// We don't worry about being signaled between the two stores.
// If we are not in a signal handler, we'll restore vdsoSP to 0,
// and no one will care about vdsoPC. If we are in a signal handler,
// we cannot receive another signal.
MOVQ 8(SP), CX
MOVQ CX, m_vdsoSP(BX)
MOVQ 0(SP), CX
MOVQ CX, m_vdsoPC(BX)
// sec is in AX, nsec in DX
// return nsec in AX
IMULQ $1000000000, AX
ADDQ DX, AX
MOVQ AX, ret+8(FP)
RET
fallback:
MOVQ $SYS_clock_gettime, AX
SYSCALL
JMP ret

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Adapted from code in the Go runtime package at Go 1.16.6:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.16
// +build !go1.17
#include "go_asm.h"
#include "textflag.h"
#define SYS_clock_gettime 113
// Hard-coded offsets into runtime structs.
// Generated by adding the following code
// to package runtime and then executing
// and empty func main:
//
// func init() {
// println("#define g_m", unsafe.Offsetof(g{}.m))
// println("#define g_sched", unsafe.Offsetof(g{}.sched))
// println("#define gobuf_sp", unsafe.Offsetof(g{}.sched.sp))
// println("#define g_stack", unsafe.Offsetof(g{}.stack))
// println("#define stack_lo", unsafe.Offsetof(g{}.stack.lo))
// println("#define m_g0", unsafe.Offsetof(m{}.g0))
// println("#define m_curg", unsafe.Offsetof(m{}.curg))
// println("#define m_vdsoSP", unsafe.Offsetof(m{}.vdsoSP))
// println("#define m_vdsoPC", unsafe.Offsetof(m{}.vdsoPC))
// println("#define m_gsignal", unsafe.Offsetof(m{}.gsignal))
// }
#define g_m 48
#define g_sched 56
#define gobuf_sp 0
#define g_stack 0
#define stack_lo 0
#define m_g0 0
#define m_curg 192
#define m_vdsoSP 832
#define m_vdsoPC 840
#define m_gsignal 80
// func monoClock(clock int) int64
TEXT ·monoClock(SB),NOSPLIT,$24-16
MOVD clock+0(FP), R22
MOVD RSP, R20 // R20 is unchanged by C code
MOVD RSP, R1
MOVD g_m(g), R21 // R21 = m
// Set vdsoPC and vdsoSP for SIGPROF traceback.
// Save the old values on stack and restore them on exit,
// so this function is reentrant.
MOVD m_vdsoPC(R21), R2
MOVD m_vdsoSP(R21), R3
MOVD R2, 8(RSP)
MOVD R3, 16(RSP)
MOVD LR, m_vdsoPC(R21)
MOVD R20, m_vdsoSP(R21)
MOVD m_curg(R21), R0
CMP g, R0
BNE noswitch
MOVD m_g0(R21), R3
MOVD (g_sched+gobuf_sp)(R3), R1 // Set RSP to g0 stack
noswitch:
SUB $32, R1
BIC $15, R1
MOVD R1, RSP
MOVW R22, R0
MOVD runtime·vdsoClockgettimeSym(SB), R2
CBZ R2, fallback
// Store g on gsignal's stack, so if we receive a signal
// during VDSO code we can find the g.
// If we don't have a signal stack, we won't receive signal,
// so don't bother saving g.
// When using cgo, we already saved g on TLS, also don't save
// g here.
// Also don't save g if we are already on the signal stack.
// We won't get a nested signal.
MOVBU runtime·iscgo(SB), R22
CBNZ R22, nosaveg
MOVD m_gsignal(R21), R22 // g.m.gsignal
CBZ R22, nosaveg
CMP g, R22
BEQ nosaveg
MOVD (g_stack+stack_lo)(R22), R22 // g.m.gsignal.stack.lo
MOVD g, (R22)
BL (R2)
MOVD ZR, (R22) // clear g slot, R22 is unchanged by C code
B finish
nosaveg:
BL (R2)
B finish
fallback:
MOVD $SYS_clock_gettime, R8
SVC
finish:
MOVD 0(RSP), R3 // sec
MOVD 8(RSP), R5 // nsec
MOVD R20, RSP // restore SP
// Restore vdsoPC, vdsoSP
// We don't worry about being signaled between the two stores.
// If we are not in a signal handler, we'll restore vdsoSP to 0,
// and no one will care about vdsoPC. If we are in a signal handler,
// we cannot receive another signal.
MOVD 16(RSP), R1
MOVD R1, m_vdsoSP(R21)
MOVD 8(RSP), R1
MOVD R1, m_vdsoPC(R21)
// sec is in R3, nsec in R5
// return nsec in R3
MOVD $1000000000, R4
MUL R4, R3
ADD R5, R3
MOVD R3, ret+8(FP)
RET

25
tstime/monocoarse_std.go Normal file
View File

@@ -0,0 +1,25 @@
// +build !go1.16 go1.17 !linux !amd64,!arm64
package tstime
import "time"
var referenceTime = time.Now()
// MonotonicCoarse returns the number of monotonic seconds elapsed
// since an unspecified starting point, at low precision.
// It is only meaningful when compared to the
// result of previous MonotonicCoarse calls.
// On some platforms, MonotonicCoarse is much faster than time.Now.
func MonotonicCoarse() int64 {
return int64(time.Since(referenceTime).Seconds())
}
// Monotonic returns the number of monotonic seconds elapsed
// since an unspecified starting point, at low precision.
// It is only meaningful when compared to the
// result of previous MonotonicCoarse calls.
// On some platforms, MonotonicCoarse is much faster than time.Now.
func Monotonic() int64 {
return int64(time.Since(referenceTime).Seconds())
}

35
tstime/monocoarse_test.go Normal file
View File

@@ -0,0 +1,35 @@
package tstime
import (
"testing"
"time"
)
func TestMonotonicCoarse(t *testing.T) {
t.Parallel()
start := MonotonicCoarse()
for n := 0; n < 30; n++ {
end := MonotonicCoarse()
if end == start {
time.Sleep(100 * time.Millisecond)
continue
}
if end-start != 1 {
t.Errorf("monotonic coarse time jumped: %v seconds", end-start)
}
return // success
}
t.Errorf("monotonic coarse time did not progress after 3s")
}
func BenchmarkMonotonic(b *testing.B) {
for i := 0; i < b.N; i++ {
Monotonic()
}
}
func BenchmarkMonotonicCoarse(b *testing.B) {
for i := 0; i < b.N; i++ {
MonotonicCoarse()
}
}