Add PHC-base-clock refclock mode (phc.sync = false) - #317
Conversation
Creates the phcsample package with the Config, wallClock, Generator, and phcWindow surface. wallClock does the monotonic-to-UTC labelling regression with gates tailored to its actual observables; Generator and phcWindow wire up as thin pass-throughs with TrueTimeOffset stubbed to ErrNotReady. Also revises the plan to reframe Config parameter semantics: phcsample inherits some field names from phcsync.ResetConfig only where meanings survive the change from pair-based pulse/message correlation to a continuous message-stream regression, and introduces new fields (MsgWindow, MaxMsgGap, MinMsgSpan, ClockRateLimit, MsgTimingVariation) for observables that exist only in the new model.
Discrete-event simulator paralleling syncsim/sim: feeds Generator with synthesized PulseEdge events and UTC time messages, installs a capture Sampler, and scores emitted samples against ground-truth true time. Covers single-edge, dual-edge, startup, missing messages/edges, gross outliers, and multi-rate (5 Hz) message delivery. Steady-state cases stay red until phcWindow.TrueTimeOffset lands in step 6, per plan. MsgRate config field strides message generation at 1/rate and applies delay+jitter per grid point, exercising the "messages can outnumber pulses" path that distinguishes phcsample's wallClock regression from phcsync's per-pulse pairing.
Fills in the phcWindow pipeline: stride-consistency + dual-edge polarity selection, edge-to-UTC labelling via wallClock, and the PHC calibration OLS fit. Adds a backward-coverage gate on wallClock so edges predating the retained message window surface as ErrNotReady rather than being labelled by backward extrapolation after a long message outage.
Three runtime modes are now explicit in gpsevent.Dispatcher: controller (PHC-disciplined), generator (PHC free-running), or neither (serial timing). A new phc.freeRunning TOML field selects between controller and generator when a PHC is present. phcsample.Generator loses its Sampler interface; the dispatcher now emits obs.NTPSample uniformly across all three modes, after rc.Sample succeeds, consistent with the existing sysSample path. NewInstance replaces the prior "drop and reconstruct via factory" approach for PHC era transitions. Plan gains two phase-2 cleanup steps (unify the pulse-edge sink interface; add logging inside phcsample) and a faster-startup motivation section.
A derivative plan that assumes phc-sample.md and describes a ppssample mode sourcing edges from the Linux kernel PPS API (/dev/ppsN). Scoped as a delta: what reuses from phcsample, what moves to a new shared gensample package, what is genuinely new. Four post-MVP input paths (pps_ldisc attach + shared tty, Linux TIOCMIWAIT, Darwin CTS polling, FreeBSD timepps.h) broken out with code-level detail. Appendix covers pure-Go Linux PPS via x/sys/unix with cgo -godefs only as a build-time constant generator.
# Conflicts: # time/app/daemon/daemon.go
Surface NTPSample in the web UI via SSE so the sys-vs-true-time offset is visible across all three dispatcher modes, not only free-running.
Apply PrePulse pulse-offset corrections to the PHC calibration window so each ruler mark lands at the true top-of-second rather than at the physical pulse edge. PulseOffset enters the fit as an X-side shift (scaled from true-time nanoseconds to PHC nanoseconds via the median pulse interval), preserving the UTC-in / UTC-out boundary established in phase 1. Add IgnoreSawtoothCorrection to phcsample.Config as an operator escape hatch and A/B toggle; Generator skips the corrector when set. timemsg.Buffer gains GetUTCPulseCorrection (float64 ns, PrePulse-only, keyed on the message's UTC derived from TAI when UTCTime is absent) and stops forwarding PrePulse messages to MsgUTCTime so their upcoming-second UTC never pollutes the wallClock / SOCK pipeline. phcsample/sim installs the buffer as the generator's pulse corrector and synthesises PrePulse correction events. The acceptance test (TestPHCSampleSawtoothCorrection) exercises a 15 ns sawtooth under both fast (250 us) and CM4/5-style (0.25 s) pulse-delivery delays, asserting that corrections visibly shrink the residual delta versus the ignored case and recover close to the zero-sawtooth baseline.
Phase 1 was already complete (steps 3-7 all merged) but the status line still listed only 3-5. Phase 2 now has its first landed step (10 - PrePulse sawtooth correction).
Info-level "generated first NTP refclock sample" on the first Observer.NTPSample call of the process lifetime, covering the step-12 warmup log generically (serial / disciplined / free-running). Wired into the daemon observer fan-out alongside the existing logobs observers. Message uses "refclock" / "generated" rather than any chrony-specific wording, matching satpulse.toml(5) terminology. Plan updated: the warmup info log leaves phcsample scope; step 12 now covers only the per-sample and failure-path debug tiers, with per-era re-firing noted as deferred pending a pause/suspend hook.
…sary
Two complementary tweaks so warm-up lands on the same pulse across
normal message-delivery-timing jitter:
- wallClock.predictUTC allows up to 500 ms backward extrapolation
before the earliest retained message's read-time. The old
threshold was points[0].tRead - expectedDelay, which floated
with per-message delay jitter; a first message that came in
slightly later than ExpectedDelay could reject an otherwise
valid early edge. The 500 ms window is generous enough to cover
any plausible edge-to-message-read skew without letting the fit
reach arbitrarily far back into stale pulse history after
recovery from a message gap. ErrNotReady (not errStale) is still
the sentinel.
- MinMsgSpan default drops from 3.0 s to 2.9 s so normal
sub-second delivery jitter on 1 Hz messages does not push
startup from pulse 5 to pulse 6.
sim: expose PulseSeed and MsgSeed on Config (defaults preserve the
former hardcoded 999/888 seeds), and track Stats.ReadyPulse - the
1-based pulse index of the first successful sample. New
TestPHCSampleWarmUpMsgSeedSweep sweeps 100 message seeds under the
default scenario and asserts ReadyPulse == 5 every time, locking in
the invariant these tweaks restore.
Add phase-2 step 15: the calibration window already discards a forward PHC step via firstGap, but a backward step leaves pre- and post-step edges in the same fit. Extend the gap detector to cover both directions.
Step 9 becomes purely about TOML wiring of [phcsample] into the daemon. Step 16 covers the man-page addition for phc.freeRunning (with a pointer to the schema for [phcsample]) and extending configs/config-schema.json to describe freeRunning and [phcsample].
Adds Config.PHCSample of type phcsample.Config, seeded from phcsample.DefaultConfig(), validated alongside Sync, and passed to phcsample.NewGenerator in free-running mode instead of the built-in defaults. go-toml's case-insensitive field matching maps the [phcsample] section to the PHCSample field, consistent with Sync / LeapSecond / PHC. Covers step 9 of plan/phc-sample.md phase 2.
Schema now covers phc.freeRunning and all 11 fields of phcsample.Config, with descriptions, defaults, and bounds kept in sync with the Go toml:, check:, and comment: tags. Also adds a matching comment: tag to PHCConfig.FreeRunning so the Go struct and schema description agree. Plan status updated: schema part of step 16 landed; man-page portion still outstanding.
Introduce gpsevent.PulseReceiver with a single Pulse(ts, tr) method so the dispatcher delivers edges without caring whether phcsync.Controller or phcsample.Generator is on the receiving end. Both types now satisfy the interface: their former exported PulseEdge structs are unexported, Controller.PulseEdge is renamed to Pulse(ts, tr), and Generator.Pulse takes (ts, tr) instead of an edge value. gpsevent.NewDispatcher takes a single PulseReceiver; a constructor-time type switch populates the typed controller/generator fields for mode-specific paths, and d.pulse is reassigned alongside d.generator on PauseEvent so post- resume edges land in the fresh instance.
Sync defaults to true; set to false to leave the PHC free-running and use [sample.phc] to configure the refclock sample generator. Inverts the boolean sense so the 8000 default behavior (discipline the PHC) is unchanged, and nests the sample-generation tuning under [sample] so a future ppssample mode fits at [sample.pps] alongside. Also trim doc comments on IgnoreSawtoothCorrection in both phcsync.TrackingConfig and phcsample.Config: drop the PrePulse specificity (inaccurate for phcsync, where both pre- and post-pulse corrections apply) and the editorializing about when to use it.
…sample step 15) Replace firstGap with firstDiscontinuity so consistentEdges restarts from the post-step suffix on forward or backward PHC steps injected by another process (e.g. a future chrony PHC-disciplining feature, or phc_ctl). The detector uses an absolute DiscontinuityThreshold (seconds; default 1 ms) and a neighbour-consistency check; Stage 4/5 PPB-based per-edge rejection is unchanged. Adds sim-rig PHCStep fault injection under a phcsample-local FaultConfig that embeds syncsim.FaultConfig, plus paired acceptance tests that run each step scenario with the detector active and effectively disabled so the clean-residual check cannot go vacuous.
ExpectedDelay 0.1 -> 0.15 MsgTimingVariation 0.05 -> 0.2 EdgeSecondTolerance 0.1 -> 0.4
Add a Logging subsection under Implementation design capturing the policy (package logs itself, TrueTimeOffset returns bool, ErrNotReady becomes strictly warmup), the rejection-to-TOML-parameter mapping, the attribution rule, and reset.go-style sticky-flag repetition suppression. Step 12 in Phase 2 becomes a pointer to that subsection.
Add prerequisites and relationship-to-other-modes section. Rewrite core per-edge calc in terms of ntime.Time. Split mode selection into orthogonal edge-source and sample-domain axes, with the invalid [pps] + phc-* combination rejected at config load. Expand the dispatcher table to cover all modes in the family.
Brings the phc-sample branch up to current master plus the ntime.Time refactor (#258). Beyond the textual conflicts, this adapts the phc-sample-only code to APIs that changed on master since the last merge, and converts the phcsample sample path to ntime.Time: - wallClock's Y axis and calibEntry.Y carry ntime.Time instead of time.Time; Generator.MsgUTCTime converts at the boundary. - timemsg and phcsample/sim use the opt.Val gpsprot accessors. - The dispatcher keeps the pulse/PulseReceiver mode dispatch from phc-sample combined with the SHM writer support from master. - replay.go keeps master's typed JSON envelope with the unified Pulse call.
|
Review map: where the additive change is, relative to the old Of the ~1040 non-test Go lines in this PR, roughly 600 are machinery carried from
Everything else is deletion (regression layer, sim rig, extrapolation gate, leap-reset scaffolding — the TAI labelling is leap-immune), type-mechanical (the |
With sync = false in the [phc] table, satpulsed leaves the PHC free-running and sends chrony SOCK samples whose tv is the PHC timestamp of each PPS edge, treated as TAI; the offset is the difference between GPS-derived TAI at the edge (with sawtooth correction) and that PHC reading. Chrony maps the PHC to the system clock on its side via its multi-clock support, so GPS-derived offsets are never re-expressed in the system-clock domain inside satpulse. Relative to the cross-sample model previously on this branch: - The regression layer goes away: each admitted pulse edge is its own sample, emitted to a new phcsample.Sampler as soon as the wallClock fit can label it. fitAndEvaluate, calibEntry, and the extrapolation gate are deleted, along with the phcsample/sim rig whose scoring measured regression error. (System-referenced free-running samples return with #256, stacked on this branch, which also introduces the ntp.clock key for selecting between the two free-running flavors.) - The Generator consumes TAI instead of UTC: timemsg.Buffer grows a MsgTAITimer sink and a TAI-keyed GetTAIPulseCorrection accessor; MsgUTCTimer is unchanged and remains the serial-mode sink. - The wallClock Y axis is domain-neutral (ntime.Time); the mode picks the domain flowing through at wiring time. - phc.sync = false is validated against the NTP settings: it requires a SOCK path and excludes SHM (whose timestamps are system-clock by definition). Observer.NTPSample does not fire in this mode: satpulse never computes a sys-vs-true offset when the PHC is free-running. End-to-end validation against multi-clock chrony on real hardware is still pending; tracked in #257.
The wallClock fit carried its own ordinary-least-squares arithmetic, and the PHC calibration fit arriving with #256 carries another copy. Pull the algorithmic core into one file - fitLine / fittedLine / lineAt / lineResidual, generic over int64- and float64-based coordinates - so it is tested directly against known points instead of only through the pipelines. The file is self-contained so it can be promoted to a library package if a consumer outside phcsample appears. The wallClock's slope-1 fallback for zero x spread is dropped: the MinMsgSpan gate guarantees x spread before the fit is consulted, so it was unreachable; fitLine reports a degenerate fit and predictRef answers ErrNotReady.
For #257.
With
sync = falsein the[phc]table, satpulsed leaves the PHC free-running and sends chrony SOCK samples whosetvis the PHC timestamp of each PPS edge, treated as TAI; the offset is the difference between GPS-derived TAI at the edge (with sawtooth correction) and that PHC reading. Chrony maps the PHC to the system clock on its side via its multi-clock support, so GPS-derived offsets are never re-expressed in the system-clock domain inside satpulse.The branch carries the shared free-running machinery developed on
phc-sample(pulse-edge admissibility, dual-edge polarity selection, the wallClock fit, PHC discontinuity detection, sawtooth correction), reconciled with current master, and reshapes emission to per-edge: the cross-sample regression, the sim rig, and everything else only the system-referenced flavor needs is deferred to #256, which stacks on this branch and ends up essentially wherephc-samplewas heading. With only one free-running flavor here,phc.sync = falsealone selects the mode; thentp.clockselector arrives with #256.Observer.NTPSampledoes not fire in this mode: satpulse never computes a sys-vs-true offset when the PHC is free-running.End-to-end validation against multi-clock chrony on real hardware is still pending (binary go/no-go per the plan in #257); this PR should not merge before that.