From 142670b8c262a5d7a16f3299889ff8b602dff70e Mon Sep 17 00:00:00 2001 From: Josh Bleecher Snyder Date: Tue, 20 Jul 2021 11:11:37 -0700 Subject: [PATCH] tstime/mono: new package Package mono provides a fast monotonic time. Its primary advantage is that it is fast: It is approximately twice as fast as time.Now. This is because time.Now uses two clock calls, one for wall time and one for monotonic time. We ask for the current time 4-6 times per network packet. At ~50ns per call to time.Now, that's enough to show up in CPU profiles. Package mono is a first step towards addressing that. It is designed to be a near drop-in replacement for package time. Signed-off-by: Josh Bleecher Snyder --- tstime/mono/mono.go | 121 +++++++++++++++++++++++++++++++++++++++ tstime/mono/mono_test.go | 30 ++++++++++ 2 files changed, 151 insertions(+) create mode 100644 tstime/mono/mono.go create mode 100644 tstime/mono/mono_test.go diff --git a/tstime/mono/mono.go b/tstime/mono/mono.go new file mode 100644 index 000000000..eda90684c --- /dev/null +++ b/tstime/mono/mono.go @@ -0,0 +1,121 @@ +// 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. + +// Package mono provides fast monotonic time. +// On most platforms, mono.Now is about 2x faster than time.Now. +// However, time.Now is really fast, and nicer to use. +// +// For almost all purposes, you should use time.Now. +// +// Package mono exists because we get the current time multiple +// times per network packet, at which point it makes a +// measurable difference. +package mono + +import ( + "fmt" + "sync/atomic" + "time" + _ "unsafe" // for go:linkname +) + +// Time is the number of nanoseconds elapsed since an unspecified reference start time. +type Time int64 + +// Now returns the current monotonic time. +func Now() Time { + // On a newly started machine, the monotonic clock might be very near zero. + // Thus mono.Time(0).Before(mono.Now.Add(-time.Minute)) might yield true. + // The corresponding package time expression never does, if the wall clock is correct. + // Preserve this correspondence by increasing the "base" monotonic clock by a fair amount. + const baseOffset int64 = 1 << 55 // approximately 10,000 hours in nanoseconds + return Time(now() + baseOffset) +} + +// Since returns the time elapsed since t. +func Since(t Time) time.Duration { + return time.Duration(Now() - t) +} + +// Sub returns t-n, the duration from n to t. +func (t Time) Sub(n Time) time.Duration { + return time.Duration(t - n) +} + +// Add returns t+d. +func (t Time) Add(d time.Duration) Time { + return t + Time(d) +} + +// After reports t > n, whether t is after n. +func (t Time) After(n Time) bool { + return t > n +} + +// After reports t < n, whether t is before n. +func (t Time) Before(n Time) bool { + return t < n +} + +// IsZero reports whether t == 0. +func (t Time) IsZero() bool { + return t == 0 +} + +// StoreAtomic does an atomic store *t = new. +func (t *Time) StoreAtomic(new Time) { + atomic.StoreInt64((*int64)(t), int64(new)) +} + +// LoadAtomic does an atomic load *t. +func (t *Time) LoadAtomic() Time { + return Time(atomic.LoadInt64((*int64)(t))) +} + +//go:linkname now runtime.nanotime1 +func now() int64 + +// baseWall and baseMono are a pair of almost-identical times used to correlate a Time with a wall time. +var ( + baseWall time.Time + baseMono Time +) + +func init() { + baseWall = time.Now() + baseMono = Now() +} + +// String prints t, including an estimated equivalent wall clock. +// This is best-effort only, for rough debugging purposes only. +// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts. +// Even in the best of circumstances, it may vary by a few milliseconds. +func (t Time) String() string { + return fmt.Sprintf("mono.Time(ns=%d, estimated wall=%v)", int64(t), baseWall.Add(t.Sub(baseMono)).Truncate(0)) +} + +// MarshalJSON formats t for JSON as if it were a time.Time. +// We format Time this way for backwards-compatibility. +// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged. +// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts. +// Even in the best of circumstances, it may vary by a few milliseconds. +func (t Time) MarshalJSON() ([]byte, error) { + var tt time.Time + if !t.IsZero() { + tt = baseWall.Add(t.Sub(baseMono)).Truncate(0) + } + return tt.MarshalJSON() +} + +// UnmarshalJSON sets t according to data. +// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged. +func (t *Time) UnmarshalJSON(data []byte) error { + var tt time.Time + err := tt.UnmarshalJSON(data) + if err != nil { + return err + } + *t = Now().Add(-time.Since(tt)) + return nil +} diff --git a/tstime/mono/mono_test.go b/tstime/mono/mono_test.go new file mode 100644 index 000000000..afa05799f --- /dev/null +++ b/tstime/mono/mono_test.go @@ -0,0 +1,30 @@ +// 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. + +package mono + +import ( + "testing" + "time" +) + +func TestNow(t *testing.T) { + start := Now() + time.Sleep(100 * time.Millisecond) + if elapsed := Since(start); elapsed < 100*time.Millisecond { + t.Errorf("short sleep: %v elapsed, want min %v", elapsed, 100*time.Millisecond) + } +} + +func BenchmarkMonoNow(b *testing.B) { + for i := 0; i < b.N; i++ { + Now() + } +} + +func BenchmarkTimeNow(b *testing.B) { + for i := 0; i < b.N; i++ { + time.Now() + } +}