diff --git a/go.mod b/go.mod index df82cce..f70cb0d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/polygon-io/go-app-ticker-wall go 1.16 require ( + github.com/dterei/gotsc v0.0.0-20160722215413-e78f872945c6 github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838 github.com/polygon-io/nanovgo v0.0.0-20210406222537-1c1e04bebee3 diff --git a/go.sum b/go.sum index 2000734..0a2e114 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dterei/gotsc v0.0.0-20160722215413-e78f872945c6 h1:iD62k/20LzNdTc2bmrOTQ4xgRbI0uJvOxTkEAmd20eQ= +github.com/dterei/gotsc v0.0.0-20160722215413-e78f872945c6/go.mod h1:P4N3xGqi52atrdlMBXpsAGTqRnLgZ8uDhlkQ7HEYGgo= github.com/go-gl/gl v0.0.0-20210315015930-ae072cafe09d h1:o81yRlBATU4PRn97lydmsq8hTRNXI4wlR/VvUQhFRVY= github.com/go-gl/gl v0.0.0-20210315015930-ae072cafe09d/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210311203641-62640a716d48 h1:QrUfZrT8n72FUuiABt4tbu8PwDnOPAbnj3Mql1UhdRI= diff --git a/time_smoother/time.go b/time_smoother/time.go new file mode 100644 index 0000000..a5fdc29 --- /dev/null +++ b/time_smoother/time.go @@ -0,0 +1,111 @@ +package time_smoother + +import ( + "math" + "time" + + "github.com/dterei/gotsc" +) + +type TimeSmoother struct { + lastRdtsc uint64 // last cycle count + lastRdtscNanos uint64 // last cycle-synced time + lastSystemTime uint64 // last system time + rdtscTime uint64 // time to call rdtsc + + clockSpeed float64 + clockSpeedDecayRate float64 + + adjustedTimeRate float64 // nanos per clock + + syncInterval uint64 + + stop chan bool +} + +func (t *TimeSmoother) Stop() { + t.stop <- true +} + +func (t *TimeSmoother) GetTime() time.Time { + tnow := gotsc.BenchStart() + + // delta in cycles + tDelta := float64(tnow - t.lastRdtsc) + + nanosNow := int64(tDelta*t.adjustedTimeRate) + int64(t.lastRdtscNanos) + + return time.Unix(0, nanosNow) +} + +// +func (t *TimeSmoother) sync(systemTime, rdtsc uint64) { + elapsedCycles := rdtsc - t.lastRdtsc + elapsedNanos := int64(systemTime) - int64(t.lastSystemTime) + + nanosNow := int64(float64(elapsedCycles)*t.adjustedTimeRate) + int64(t.lastRdtscNanos) // current extrapolated time + + newClock := float64(elapsedNanos) / float64(elapsedCycles) + t.clockSpeed = t.clockSpeedDecayRate*t.clockSpeed + (1-t.clockSpeedDecayRate)*newClock + + targetSystemTime := int64(systemTime + t.syncInterval) + timeRate := float64(targetSystemTime-nanosNow) / t.clockSpeed + if timeRate < 0 { + timeRate = 0.0 + } + + t.adjustedTimeRate = timeRate + t.lastSystemTime = systemTime + t.lastRdtsc = rdtsc + t.lastRdtscNanos = uint64(nanosNow) +} + +func (t *TimeSmoother) daemon() { + ticker := time.NewTicker(time.Duration(t.syncInterval)) + for { + select { + case <-ticker.C: + systemTime := time.Now().UnixNano() + rdtsc := gotsc.BenchStart() - t.rdtscTime + t.sync(uint64(systemTime), rdtsc) + case <-t.stop: + return + } + } +} + +// gets an initial estimate of the system clock speed +func (t *TimeSmoother) initClock() { + sys1 := time.Now() + rdtsc1 := gotsc.BenchStart() + time.Sleep(time.Second) + rdtsc2 := gotsc.BenchEnd() + sysDelta := time.Since(sys1) + + cycles := rdtsc2 - rdtsc1 - 2*t.rdtscTime + elapsedTime := sysDelta.Nanoseconds() + + clockSpeed := float64(elapsedTime) / float64(cycles) + t.clockSpeed = clockSpeed +} + +func NewTimeSmoother() *TimeSmoother { + smoother := TimeSmoother{} + smoother.rdtscTime = gotsc.TSCOverhead() / 2 // this function measures calling twice + smoother.lastSystemTime = uint64(time.Now().UnixNano()) + smoother.lastRdtsc = gotsc.BenchStart() - smoother.rdtscTime + smoother.lastRdtscNanos = smoother.lastSystemTime + + smoother.initClock() + smoother.adjustedTimeRate = smoother.clockSpeed + + half_life := 10.0 // 10 second half life + smoother.clockSpeedDecayRate = math.Pow(2, -1.0/half_life) + smoother.syncInterval = 100_000_000 // 100 ms + + smoother.stop = make(chan bool) + + go smoother.daemon() + + return &smoother +}