summaryrefslogtreecommitdiff
path: root/vendor/codeberg.org/gruf/go-nowish/clock.go
blob: 781e59f182d5213b2354b449c4c16d7e6f897211 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package nowish

import (
	"sync"
	"sync/atomic"
	"time"
	"unsafe"
)

// Start returns a new Clock instance initialized and
// started with the provided precision, along with the
// stop function for it's underlying timer
func Start(precision time.Duration) (*Clock, func()) {
	c := Clock{}
	return &c, c.Start(precision)
}

type Clock struct {
	// format stores the time formatting style string
	format string

	// valid indicates whether the current value stored in .Format is valid
	valid uint32

	// mutex protects writes to .Format, not because it would be unsafe, but
	// because we want to minimize unnnecessary allocations
	mutex sync.Mutex

	// nowfmt is an unsafe pointer to the last-updated time format string
	nowfmt unsafe.Pointer

	// now is an unsafe pointer to the last-updated time.Time object
	now unsafe.Pointer
}

// Start starts the clock with the provided precision, the returned
// function is the stop function for the underlying timer. For >= 2ms,
// actual precision is usually within AT LEAST 10% of requested precision,
// less than this and the actual precision very quickly deteriorates.
func (c *Clock) Start(precision time.Duration) func() {
	// Create ticker from duration
	tick := time.NewTicker(precision / 10)

	// Set initial time
	t := time.Now()
	atomic.StorePointer(&c.now, unsafe.Pointer(&t))

	// Set initial format
	s := ""
	atomic.StorePointer(&c.nowfmt, unsafe.Pointer(&s))

	// If formatting string unset, set default
	c.mutex.Lock()
	if c.format == "" {
		c.format = time.RFC822
	}
	c.mutex.Unlock()

	// Start main routine
	go c.run(tick)

	// Return stop fn
	return tick.Stop
}

// run is the internal clock ticking loop.
func (c *Clock) run(tick *time.Ticker) {
	for {
		// Wait on tick
		_, ok := <-tick.C

		// Channel closed
		if !ok {
			break
		}

		// Update time
		t := time.Now()
		atomic.StorePointer(&c.now, unsafe.Pointer(&t))

		// Invalidate format string
		atomic.StoreUint32(&c.valid, 0)
	}
}

// Now returns a good (ish) estimate of the current 'now' time.
func (c *Clock) Now() time.Time {
	return *(*time.Time)(atomic.LoadPointer(&c.now))
}

// NowFormat returns the formatted "now" time, cached until next tick and "now" updates.
func (c *Clock) NowFormat() string {
	// If format still valid, return this
	if atomic.LoadUint32(&c.valid) == 1 {
		return *(*string)(atomic.LoadPointer(&c.nowfmt))
	}

	// Get mutex lock
	c.mutex.Lock()

	// Double check still invalid
	if atomic.LoadUint32(&c.valid) == 1 {
		c.mutex.Unlock()
		return *(*string)(atomic.LoadPointer(&c.nowfmt))
	}

	// Calculate time format
	nowfmt := c.Now().Format(c.format)

	// Update the stored value and set valid!
	atomic.StorePointer(&c.nowfmt, unsafe.Pointer(&nowfmt))
	atomic.StoreUint32(&c.valid, 1)

	// Unlock and return
	c.mutex.Unlock()
	return nowfmt
}

// SetFormat sets the time format string used by .NowFormat().
func (c *Clock) SetFormat(format string) {
	// Get mutex lock
	c.mutex.Lock()

	// Update time format
	c.format = format

	// Invalidate current format string
	atomic.StoreUint32(&c.valid, 0)

	// Unlock
	c.mutex.Unlock()
}