Documentation
¶
Overview ¶
Package pll keeps time for programs that must produce or consume things at a steady rate, or follow a clock other than their own.
A Pacer divides time into evenly spaced, numbered slots at an exact Rate and says which slots are due. Every slot's time is computed from its number with exact rational arithmetic, so it never drifts, even at rates such as 30000/1001 units per second. A Pacer can run slightly faster or slower than its nominal rate through a trim, given in parts per trillion.
A Follower is a phase-locked loop. Fed readings of a remote clock taken against the local one, it learns how far apart the two clocks are and how fast the remote one runs, filters out delivery jitter and outliers, notices when the remote clock jumps, and maps any remote time to local time and back. Its Trim can drive a Pacer, so output keeps pace with a device that runs on its own clock.
The package never reads a clock, starts a goroutine or touches your data. Every call that depends on the present takes a time you pass in, a duration from an origin you choose, and the hot paths allocate nothing.
Index ¶
- Variables
- type ConfigError
- type Follower
- func (f *Follower) Jitter() time.Duration
- func (f *Follower) Local(remote int64) (time.Duration, error)
- func (f *Follower) Observe(local time.Duration, remote int64) (Observation, error)
- func (f *Follower) Offset() float64
- func (f *Follower) Remote(local time.Duration) (int64, error)
- func (f *Follower) Reset()
- func (f *Follower) State(now time.Duration) State
- func (f *Follower) Trim() int64
- type FollowerConfig
- type Observation
- type Pacer
- func (p *Pacer) Claim(n uint64) (Span, error)
- func (p *Pacer) Due(now time.Duration, max uint64) (Span, error)
- func (p *Pacer) NextDue() (slot uint64, at time.Duration, err error)
- func (p *Pacer) SetTrim(ppt int64) error
- func (p *Pacer) Skip(now time.Duration, keep uint64) (uint64, error)
- func (p *Pacer) Trim() int64
- type Rate
- type Span
- type State
- type Status
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidRate reports a rate with a zero numerator or denominator, // including the zero Rate. ErrInvalidRate = errors.New("pll: invalid rate") // ErrInvalidArgument reports a negative time, a zero count, or a trim // outside the open interval (-1e12, 1e12). ErrInvalidArgument = errors.New("pll: invalid argument") // ErrOverflow reports a slot index or a slot time that does not fit. ErrOverflow = errors.New("pll: overflow") )
var ( // ErrOrder reports a reading whose local time went backwards or whose // remote ticks did not increase. The reading is ignored. ErrOrder = errors.New("pll: reading out of order") // ErrNotAnchored reports a mapping requested before the first reading. ErrNotAnchored = errors.New("pll: follower has no reading yet") )
Functions ¶
This section is empty.
Types ¶
type ConfigError ¶
type ConfigError struct {
Field string // the field name, such as "Bandwidth"
Reason string // what the field must satisfy, such as "must be finite and > 0"
}
ConfigError reports an invalid FollowerConfig field.
func (*ConfigError) Error ¶
func (e *ConfigError) Error() string
Error returns, for example, "pll: FollowerConfig.Bandwidth must be finite and > 0".
type Follower ¶
type Follower struct {
// contains filtered or unexported fields
}
Follower is a second-order (type-II) phase-locked loop that tracks a remote clock against the local clock from (local time, remote ticks) readings. It is not safe for concurrent use. Construct it with NewFollower.
Example ¶
Put a sensor's readings on the local timeline. The sensor counts microseconds on its own crystal, which runs 50 ppm fast.
package main
import (
"fmt"
"time"
"github.com/version2beta/pll"
)
func main() {
nominal, _ := pll.UnitsPerSecond(1_000_000, 1)
f, _ := pll.NewFollower(pll.DefaultFollowerConfig(nominal))
sensor := func(local time.Duration) int64 { // the sensor's counter at local time
return int64(local.Seconds() * 1e6 * (1 + 50e-6))
}
for local := time.Duration(0); local <= time.Minute; local += 10 * time.Millisecond {
f.Observe(local, sensor(local))
}
when, _ := f.Local(sensor(time.Minute + 500*time.Millisecond))
fmt.Printf("rate offset %.1f ppm, %v\n", f.Offset()*1e6, f.State(time.Minute))
fmt.Println("a reading taken at 60.5 s maps to", when.Round(time.Microsecond))
}
Output: rate offset 50.0 ppm, locked a reading taken at 60.5 s maps to 1m0.5s
func NewFollower ¶
func NewFollower(cfg FollowerConfig) (*Follower, error)
NewFollower returns a Follower with no readings. An invalid field returns nil and a *ConfigError naming the first bad field in declaration order.
func (*Follower) Jitter ¶
Jitter returns the root mean square of accepted samples' phase errors, weighted 1/16 per sample, rounded to the nearest nanosecond. It is 0 until two samples have been accepted since the last anchor, step or Reset.
func (*Follower) Observe ¶
Observe feeds one reading: the remote clock showed remote ticks at local time local. It returns what happened to the reading and, for a sample fed to the loop, its phase error. A reading out of order returns ErrOrder, and a reading whose loop update would move the anchor beyond the range of time.Duration returns ErrOverflow; either way nothing changes.
func (*Follower) Offset ¶
Offset returns the estimated fractional rate offset y: the remote clock runs at nominal × (1 + y) as measured by the local clock.
func (*Follower) Reset ¶
func (f *Follower) Reset()
Reset forgets every reading, the rate estimate included; the next reading anchors.
func (*Follower) State ¶
State returns StateHoldover when now is more than HoldoverAfter past the last accepted sample, and otherwise the lock state.
func (*Follower) Trim ¶
Trim returns round(y × 1e12), ready for Pacer.SetTrim.
Example ¶
Keep a device with its own clock fed: the units it has consumed are a reading of its clock, and the follower's trim makes the pacer keep up.
package main
import (
"fmt"
"time"
"github.com/version2beta/pll"
)
func main() {
rate, _ := pll.UnitsPerSecond(48000, 1)
p, _ := pll.NewPacer(rate)
f, _ := pll.NewFollower(pll.DefaultFollowerConfig(rate))
consumed := func(t time.Duration) int64 { // the device runs 40 ppm fast
return int64(t.Seconds() * 48000 * (1 + 40e-6))
}
for t := time.Duration(0); t <= 5*time.Minute; t += 100 * time.Millisecond {
f.Observe(t, consumed(t))
_ = p.SetTrim(f.Trim())
}
fmt.Printf("pacer trim %.0f ppm\n", float64(p.Trim())/1e6)
}
Output: pacer trim 40 ppm
type FollowerConfig ¶
type FollowerConfig struct {
// Nominal is the remote clock's nominal tick rate, in ticks per second.
Nominal Rate
// Bandwidth is the loop's natural frequency in Hz. Lower filters more
// jitter and settles more slowly.
Bandwidth float64
// Damping is the loop's damping ratio.
Damping float64
// MaxOffsetPPM bounds the rate offset the loop will estimate, in parts
// per million.
MaxOffsetPPM float64
// Window groups readings into windows of this much local time and feeds
// the loop only the earliest-looking reading of each. 0 feeds every reading.
Window time.Duration
// OutlierThreshold: a sample whose phase error exceeds this is rejected.
OutlierThreshold time.Duration
// StepAfter is how many consecutive, agreeing rejections force a phase step.
StepAfter int
// LockThreshold is the largest phase error that counts toward lock.
LockThreshold time.Duration
// LockCount is how many consecutive good samples declare lock.
LockCount int
// HoldoverAfter is how long without an accepted sample before State
// reports holdover.
HoldoverAfter time.Duration
}
FollowerConfig configures a Follower. Start from DefaultFollowerConfig and change what you need; NewFollower checks every field.
func DefaultFollowerConfig ¶
func DefaultFollowerConfig(nominal Rate) FollowerConfig
DefaultFollowerConfig returns the defaults for a remote clock with the given nominal tick rate: Bandwidth 0.1 Hz, Damping √2/2, MaxOffsetPPM 500, Window 0, OutlierThreshold 5 ms, StepAfter 3, LockThreshold 100 µs, LockCount 8, HoldoverAfter 5 s.
type Observation ¶
type Observation struct {
Status Status
Error time.Duration // phase error of the sample fed to the loop, ns
}
Observation reports what Observe did with a reading.
type Pacer ¶
type Pacer struct {
// contains filtered or unexported fields
}
Pacer divides time into evenly spaced slots numbered 0, 1, 2, … at an exact rate and hands them out in order. Slot 0 is due at time zero. A cursor marks the next slot not yet handed out or skipped; it never moves back.
Times are durations from an origin the caller chooses; the Pacer never reads a clock. A Pacer is not safe for concurrent use. Construct it with NewPacer; the zero Pacer is not usable.
func NewPacer ¶
NewPacer returns a Pacer for rate r with no trim. An invalid Rate returns nil and ErrInvalidRate.
func (*Pacer) Claim ¶
Claim hands out the next n slots whether or not they are due, and moves the cursor past them.
Errors: ErrInvalidArgument when n is 0; ErrOverflow when the span would reach slot math.MaxUint64 or its last slot's time would exceed the largest time.Duration. On error nothing changes.
Example ¶
Send variable-size datagrams at 50 Mbit/s: one slot per byte.
package main
import (
"fmt"
"github.com/version2beta/pll"
)
func main() {
rate, _ := pll.BitsPerSecond(50_000_000, 8)
p, _ := pll.NewPacer(rate)
for _, size := range []uint64{1500, 400, 1500} {
span, _ := p.Claim(size)
fmt.Printf("%4d bytes: send at %v\n", size, span.Time(0))
}
}
Output: 1500 bytes: send at 0s 400 bytes: send at 240µs 1500 bytes: send at 304µs
func (*Pacer) Due ¶
Due hands out the oldest slots whose exact time is at or before now, at most max of them, and moves the cursor past them. When nothing is due it returns a Span with Count 0 and First at the cursor. A now earlier than before is not an error; it finds nothing new.
Errors: ErrInvalidArgument when now < 0 or max is 0; ErrOverflow when the cursor has reached math.MaxUint64 and that slot is due. On error nothing changes.
Example ¶
Pace audio samples at 48 kHz: wake on any cadence and write what came due.
package main
import (
"fmt"
"time"
"github.com/version2beta/pll"
)
func main() {
rate, _ := pll.UnitsPerSecond(48_000, 1)
p, _ := pll.NewPacer(rate)
for _, now := range []time.Duration{0, 10 * time.Millisecond, 25 * time.Millisecond} {
span, _ := p.Due(now, 4096)
fmt.Printf("at %v: %d samples, slots %d-%d\n", now, span.Count, span.First, span.First+span.Count-1)
}
}
Output: at 0s: 1 samples, slots 0-0 at 10ms: 480 samples, slots 1-480 at 25ms: 720 samples, slots 481-1200
func (*Pacer) NextDue ¶
NextDue returns the cursor slot and the earliest whole nanosecond at which it is due: the ceiling of its exact time. A Due call at that time or later hands it out.
Errors: ErrOverflow when the cursor has reached math.MaxUint64 or that time exceeds the largest time.Duration; slot and at are then zero.
func (*Pacer) SetTrim ¶
SetTrim sets the rate to nominal × (1 + ppt/1e12) for the cursor slot and every slot after it. Spans already handed out keep their times.
The new timing starts at the floor of the cursor slot's exact time under the old timing, so the phase moves by less than 1 ns and times never decrease. Setting the trim it already has changes nothing.
Errors: ErrInvalidArgument when ppt <= -1e12 or ppt >= 1e12; ErrOverflow when the cursor slot's time exceeds the largest time.Duration. On error nothing changes.
Example ¶
Follow a device whose clock runs 50 ppm fast.
package main
import (
"fmt"
"github.com/version2beta/pll"
)
func main() {
rate, _ := pll.UnitsPerSecond(1000, 1)
p, _ := pll.NewPacer(rate)
p.Claim(1000)
_ = p.SetTrim(50_000_000) // 50 ppm = 50,000,000 parts per trillion
span, _ := p.Claim(1001)
fmt.Println(span.Time(1000))
}
Output: 1.999950002s
func (*Pacer) Skip ¶
Skip moves the cursor past the slots that are due at now without handing them out, except the newest keep of them, and returns how many it skipped. Use it when there is nothing to put in old slots: Skip(now, k) followed by Due(now, k) hands out the newest k due slots.
Errors: ErrInvalidArgument when now < 0; ErrOverflow when the cursor has reached math.MaxUint64 and that slot is due. On error nothing changes.
type Rate ¶
type Rate struct {
// contains filtered or unexported fields
}
Rate is an exact number of units per second, held as a fraction reduced to lowest terms. Construct one with UnitsPerSecond or BitsPerSecond; the zero Rate is invalid.
func BitsPerSecond ¶
BitsPerSecond returns the rate of bitsPerUnit-bit units that carries bits bits per second: bits/bitsPerUnit units per second, reduced to lowest terms. BitsPerSecond(50_000_000, 8) is 6250000 units (bytes) per second. A zero argument returns the zero Rate and ErrInvalidRate.
func UnitsPerSecond ¶
UnitsPerSecond returns the rate num/den units per second, reduced to lowest terms: UnitsPerSecond(60000, 2002) has Num 30000 and Den 1001. A zero num or den returns the zero Rate and ErrInvalidRate.
type Span ¶
type Span struct {
First, Count uint64
// contains filtered or unexported fields
}
Span is a run of consecutive slots handed out by a Pacer: slots First through First+Count-1. It carries the timing it was issued under, so a later SetTrim never changes its times. A Span is a small value; copy it freely.
func (Span) All ¶
All yields each slot in the span with its time, in order.
Example ¶
A rate that is not a whole number of units per second stays exact forever.
package main
import (
"fmt"
"github.com/version2beta/pll"
)
func main() {
rate, _ := pll.UnitsPerSecond(30000, 1001)
p, _ := pll.NewPacer(rate)
span, _ := p.Claim(4)
for slot, at := range span.All() {
fmt.Println(slot, at)
}
fmt.Println(rate)
}
Output: 0 0s 1 33.366666ms 2 66.733333ms 3 100.1ms 30000/1001 units/s
type State ¶
type State uint8
State is a follower's lock state.
const ( // StateAcquiring: anchored or not yet, but not locked. StateAcquiring State = iota // StateLocked: LockCount consecutive accepted samples were within LockThreshold. StateLocked // StateHoldover: no sample accepted for more than HoldoverAfter; the // mapping keeps running at the last rate estimate. StateHoldover )
type Status ¶
type Status uint8
Status says what Observe did with a reading.
const ( // StatusAnchored: the first reading after NewFollower or Reset set the mapping. StatusAnchored Status = iota + 1 // StatusAccepted: a sample was fed to the loop and used. StatusAccepted // StatusPending: the reading joined the current lower-envelope window. StatusPending // StatusRejected: the sample fed to the loop was an outlier and was ignored. StatusRejected // StatusStepped: enough agreeing outliers showed that the remote clock // jumped; the loop re-anchored on the latest one. StatusStepped )
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
pllsim
command
Command pllsim runs the simulator scenarios against the Follower and prints what each one measured.
|
Command pllsim runs the simulator scenarios against the Follower and prints what each one measured. |
|
internal
|
|
|
sim
Package sim is a deterministic simulator for exercising pll: remote clocks that are offset, fast or slow, drifting, changing rate or jumping; a delivery link that adds jitter, heavy-tailed delays, batching and outages; a device that drains a buffer at its own rate; and a caller that wakes up late.
|
Package sim is a deterministic simulator for exercising pll: remote clocks that are offset, fast or slow, drifting, changing rate or jumping; a delivery link that adds jitter, heavy-tailed delays, batching and outages; a device that drains a buffer at its own rate; and a caller that wakes up late. |