Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mono-docs-and-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@livekit/protocol": patch
---

Document mono package, support Unix and Parse.
54 changes: 53 additions & 1 deletion utils/mono/mono.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package mono enforces use of monotonic time when creating/parsing time.Time from external sources.
//
// Using time.Now produces monotonic time values that correctly measure time difference in the presence of clock resets.
//
// On the other hand, time produce by time.Unix or time.Parse doesn't have this property. Clock reset may lead to incorrect
// durations computed from these timestamps. To fix this, prefer using Unix and Parse provided by this package.
//
// Monotonic time could also be erased when using functions like Truncate, Round, In, UTC. Be careful when using these.
//
// More details: https://go.googlesource.com/proposal/+/master/design/12914-monotonic.md
package mono

import "time"
Expand All @@ -21,21 +31,63 @@ var (
epochNano = epoch.UnixNano()
)

// resetClock resets the reference timestamp.
// Used in tests only.
func resetClock() {
epoch = time.Now()
epochNano = epoch.UnixNano()
}

// jumpClock adjusts reference timestamp by a given duration emulating a clock reset/jump.
// Used in tests only.
func jumpClock(dt time.Duration) {
epoch = epoch.Add(-dt) // we pretend time.Now() jumps, not the reference
epochNano = epoch.UnixNano()
}

// FromTime ensures that time.Time value uses monotonic clock.
//
// Deprecated: You should probably use Unix or Parse instead.
func FromTime(t time.Time) time.Time {
return fromTime(t)
}

func fromTime(t time.Time) time.Time {
if t.IsZero() {
return time.Time{}
}
return epoch.Add(t.Sub(epoch))
}

// Now is a wrapper for time.Time.
//
// Deprecated: time.Now always uses monotonic clock.
func Now() time.Time {
return epoch.Add(time.Since(epoch))
return time.Now()
}

// Unix is an analog of time.Unix that produces monotonic time.
func Unix(sec, nsec int64) time.Time {
return fromTime(time.Unix(sec, nsec))
}

// Parse is an analog of time.Parse that produces monotonic time.
func Parse(layout, value string) (time.Time, error) {
t, err := time.Parse(layout, value)
if err != nil {
return time.Time{}, err
}
return fromTime(t), nil
}

// UnixNano returns the number of nanoseconds elapsed, based on the application start time.
// This value may be different from time.Now().UnixNano() in the presence of time resets.
func UnixNano() int64 {
return epochNano + int64(time.Since(epoch))
}

// UnixMicro returns the number of microseconds elapsed, based on the application start time.
// This value may be different from time.Now().UnixMicro() in the presence of time resets.
func UnixMicro() int64 {
return UnixNano() / 1000
}
17 changes: 16 additions & 1 deletion utils/mono/mono_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ import (

func TestMonoZero(t *testing.T) {
ts := time.Time{}
ts2 := FromTime(ts)
ts2 := fromTime(ts)
require.True(t, ts.IsZero())
require.True(t, ts2.IsZero())
require.True(t, ts.Equal(ts2))
require.Equal(t, ts.String(), ts2.String())
}

func TestMono(t *testing.T) {
t.Cleanup(resetClock) // restore

ts1 := time.Now()
ts2 := ts1.Add(time.Second)

ts1m := fromTime(ts1)
// emulate a clock reset, +1h jump
// TODO: use synctest when we switch to Go 1.25
jumpClock(time.Hour)
ts2m := fromTime(ts2)

require.Equal(t, ts2.Sub(ts1), ts2m.Sub(ts1m))
}
Loading