From e45b6c9d5d74d05d171fbe526f86b79db583af59 Mon Sep 17 00:00:00 2001 From: Dom Dwyer Date: Thu, 19 Apr 2018 11:06:52 +0100 Subject: [PATCH 1/2] socket: amortise cost of querying OS time counter #116 adds a much needed ability to shrink the connection pool, but requires tracking the last-used timestamp for each socket after every operation. Frequent calls to time.Now() in the hot-path reduced read throughput by ~6% and increased the latency (and variance) of socket operations as a whole. This PR adds a periodically updated time value to amortise the cost of the last- used bookkeeping, restoring the original throughput at the cost of approximate last-used values (configured to be ~25ms of potential skew). On some systems (currently including FreeBSD) querying the time counter also requires a syscall/context switch. Fixes #142. --- coarse_time.go | 62 +++++++++++++++++++++++++++++++++++++++++++++ coarse_time_test.go | 23 +++++++++++++++++ server.go | 14 +++++++++- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 coarse_time.go create mode 100644 coarse_time_test.go diff --git a/coarse_time.go b/coarse_time.go new file mode 100644 index 000000000..e54dd17cf --- /dev/null +++ b/coarse_time.go @@ -0,0 +1,62 @@ +package mgo + +import ( + "sync" + "sync/atomic" + "time" +) + +// coarseTimeProvider provides a periodically updated (approximate) time value to +// amortise the cost of frequent calls to time.Now. +// +// A read throughput increase of ~6% was measured when using coarseTimeProvider with the +// high-precision event timer (HPET) on FreeBSD 11.1 and Go 1.10.1 after merging +// #116. +// +// Calling Now returns a time.Time that is updated at the configured interval, +// however due to scheduling the value may be marginally older than expected. +// +// coarseTimeProvider is safe for concurrent use. +type coarseTimeProvider struct { + once sync.Once + stop chan struct{} + last atomic.Value +} + +// Now returns the most recently acquired time.Time value. +func (t *coarseTimeProvider) Now() time.Time { + return t.last.Load().(time.Time) +} + +// Close stops the periodic update of t. +// +// Any subsequent calls to Now will return the same value forever. +func (t *coarseTimeProvider) Close() { + t.once.Do(func() { + close(t.stop) + }) +} + +// newcoarseTimeProvider returns a coarseTimeProvider configured to update at granularity. +func newcoarseTimeProvider(granularity time.Duration) *coarseTimeProvider { + t := &coarseTimeProvider{ + stop: make(chan struct{}), + } + + t.last.Store(time.Now()) + + go func() { + ticker := time.NewTicker(granularity) + for { + select { + case <-t.stop: + ticker.Stop() + return + case <-ticker.C: + t.last.Store(time.Now()) + } + } + }() + + return t +} diff --git a/coarse_time_test.go b/coarse_time_test.go new file mode 100644 index 000000000..284a9c246 --- /dev/null +++ b/coarse_time_test.go @@ -0,0 +1,23 @@ +package mgo + +import ( + "testing" + "time" +) + +func TestcoarseTimeProvider(t *testing.T) { + t.Parallel() + + const granularity = 50 * time.Millisecond + + ct := newcoarseTimeProvider(granularity) + defer ct.Close() + + start := ct.Now().Unix() + time.Sleep(time.Second) + + got := ct.Now().Unix() + if got <= start { + t.Fatalf("got %d, expected at least %d", got, start) + } +} diff --git a/server.go b/server.go index 7832bec1b..f34624f74 100644 --- a/server.go +++ b/server.go @@ -36,6 +36,18 @@ import ( "github.com/globalsign/mgo/bson" ) +// coarseTime is used to amortise the cost of querying the timecounter (possibly +// incurring a syscall too) when setting a socket.lastTimeUsed value which +// happens frequently in the hot-path. +// +// The lastTimeUsed value may be skewed by at least 25ms (see +// coarseTimeProvider). +var coarseTime *coarseTimeProvider + +func init() { + coarseTime = newcoarseTimeProvider(25 * time.Millisecond) +} + // --------------------------------------------------------------------------- // Mongo server encapsulation. @@ -293,7 +305,7 @@ func (server *mongoServer) close(waitForIdle bool) { func (server *mongoServer) RecycleSocket(socket *mongoSocket) { server.Lock() if !server.closed { - socket.lastTimeUsed = time.Now() + socket.lastTimeUsed = coarseTime.Now() // A rough approximation of the current time - see courseTime server.unusedSockets = append(server.unusedSockets, socket) } // If anybody is waiting for a connection, they should try now. From 21e0f0a6cb7ff1a6f816762796c800cbc161e1f0 Mon Sep 17 00:00:00 2001 From: Dom Dwyer Date: Thu, 19 Apr 2018 11:25:08 +0100 Subject: [PATCH 2/2] coarse-time: keep go vet happy --- coarse_time_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coarse_time_test.go b/coarse_time_test.go index 284a9c246..5c98fc65d 100644 --- a/coarse_time_test.go +++ b/coarse_time_test.go @@ -5,7 +5,7 @@ import ( "time" ) -func TestcoarseTimeProvider(t *testing.T) { +func TestCoarseTimeProvider(t *testing.T) { t.Parallel() const granularity = 50 * time.Millisecond