score::time — Unified Clock Interface#
Overview#
score::time provides a unified, clock-domain-agnostic API for reading time
snapshots, checking clock readiness, and subscribing to clock synchronization events —
all through a single template wrapper Clock<Tag>.
The design separates two concerns:
What kind of time — expressed as a tag struct (
VehicleTime,HighResSteadyTime,std::chrono::steady_clock,std::chrono::system_clock).How to access it — always via
Clock<Tag>::GetInstance(); clock-domain selection is a compile-time decision, enforced by the type system.
Clock domains#
Clock alias |
Tag |
Status concept |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
VehicleTime#
VehicleTime is a PTP-synchronized timebase driven by the network Grand Master clock.
Each Now() call returns a ClockSnapshot that bundles the timepoint with a
VehicleTimeStatus — a set of quality flags (kSynchronized, kTimeOut,
kTimeLeapFuture, kTimeLeapPast) and a rate-deviation measurement. The flags let
callers decide whether the time value is reliable enough for their use case without
making a separate status call.
Because VehicleTime depends on an IPC channel to the
TimeDaemon, it requires an explicit Init() call before
Now() returns synchronized data. Readiness can be probed non-blocking via
IsAvailable() or waited for with WaitUntilAvailable(). Callers can also
subscribe to synchronization events (status changes, sync messages, peer-delay
measurements) via Clock<VehicleTime>::Subscribe<E>().
HighResSteadyTime#
HighResSteadyTime is a monotonic, nanosecond-resolution clock optimized for
low-overhead timing. On QNX the backend reads the hardware cycle counter directly via
ClockCycles() — no kernel call, no scheduler interaction. On Linux it delegates to
std::chrono::high_resolution_clock. It carries NoStatus and is always ready: no
Init() is needed and IsAvailable() / WaitUntilAvailable() are not available
(calling them is a compile error). Use it for tight timing loops and deadline checks
where call overhead matters.
SteadyClock#
SteadyClock wraps std::chrono::steady_clock (POSIX CLOCK_MONOTONIC). It is
monotonic and never goes backward, making it the standard choice for measuring elapsed
time and computing timeouts. It carries NoStatus and requires no initialization.
SystemClock#
SystemClock wraps std::chrono::system_clock (POSIX CLOCK_REALTIME). It
represents wall-clock (UTC-based) time and may be adjusted or jump forward or backward.
It carries NoStatus and requires no initialization. Use it when a calendar
timestamp is needed — not for measuring elapsed time or computing timeouts.
Architecture#
The library has three layers:
Public headers under
score/time/<domain>/— tag structs and callback types that clients include directly.Framework layer under
score/time/clock/— theClock<Tag>wrapper, traits, subscription hooks, and the test utilities (clock_test_utilsBazel target). This layer has no backend dependency. Theclock_test_utilstarget (scoped_clock_override.h,clock_test_factory.h) istestonlyand must not appear in production deps.Internal under
score/time/<domain>/details/— pure-virtual backend interfaces and production implementations. Clients must never include anything from adetails/subfolder.
Class overview#
Core types#
Clock<Tag>#
The sole user-facing handle for a clock domain. A cheaply copyable value type — all
copies share the same backend instance via a shared_ptr. Its API surface is
intentionally uniform across all domains: Now() for reading, Subscribe /
Unsubscribe for events, Init / IsAvailable / WaitUntilAvailable for
readiness — with opt-in capabilities gated at compile time by the hook templates below.
ClockTraits<Tag>#
The domain registration point. The primary template is intentionally incomplete; each
clock domain provides a full explicit specialisation that binds together the backend
type, duration, timepoint, snapshot, and the CallNow factory function. No existing
file is modified when a new domain is added.
ClockSnapshot<TimepointT, StatusT>#
The immutable return value of every Now() call. Bundles the timepoint and its
quality metadata into a single atomic read — no separate status call is ever needed.
TimepointT is std::chrono::time_point<Tag, Duration>, making different clock
domains’ timepoints incompatible types so cross-domain arithmetic is a compile error.
StatusT is the domain’s chosen metadata type (see below).
StatusT, ClockStatus<FlagEnumT>, and NoStatus#
All domain-specific metadata lives in StatusT — ClockSnapshot itself is never
extended. Two building blocks are provided: NoStatus (zero-size placeholder for
always-ready clocks with no quality concept) and ClockStatus<FlagEnumT> (generic
bitmask over a scoped flag enum). A domain may use either alone or compose them inside
a richer struct alongside continuous fields — as VehicleTimeStatus does with
ClockStatus<VehicleTime::StatusFlag> and double rate_deviation.
Capability hooks#
Three SFINAE hook templates gate the optional capabilities of Clock<Tag>. Each
primary template is intentionally undefined — using an ungated capability on a domain
that has not opted in is a compile error, not a runtime failure:
InitializationHook<Tag> unlocks Init(), AvailabilityHook<Tag> unlocks
IsAvailable() and WaitUntilAvailable(), and SubscriptionHook<Tag, EventType>
unlocks Subscribe / Unsubscribe for a specific event type.
Use Cases#
VehicleTime#
VT1 — Time polling with status check#
Obtain a snapshot and inspect the synchronization quality before using the time value.
Now() returns a single immutable ClockSnapshot — the timepoint and its
VehicleTimeStatus are always fetched together, with no separate status call needed.
#include "score/time/vehicle_time/src/vehicle_clock.h"
void MyComponent::CheckTime()
{
auto clock = score::time::VehicleClock::GetInstance();
auto snapshot = clock.Now();
if (snapshot.Status().IsReliable()) {
auto tp = snapshot.TimePoint();
// use tp ...
} else if (snapshot.Status().IsFlagActive(
score::time::VehicleTime::StatusFlag::kTimeOut)) {
HandleTimeout();
}
}
Note
Init() must be called once during application startup before Now() is expected
to return synchronized data (see VT2). Without it, Now() returns a snapshot with
no flags set (IsConsistent() returns false).
Status flags:
Flag |
Meaning |
|---|---|
|
Synchronized at least once to the PTP Grand Master |
|
No sync message received within the configured time window |
|
A large forward adjustment was applied |
|
A large backward adjustment was applied |
VehicleTimeStatus::IsReliable() returns true only when kSynchronized is set
and none of {kTimeOut, kTimeLeapFuture, kTimeLeapPast} is set.
VehicleTimeStatus::HasBeenSynchronized() returns true whenever kSynchronized
has been set at least once during this lifecycle, regardless of current fault flags.
VehicleTimeStatus::IsConsistent() checks that the flag combination is internally
valid (at least one flag set, and not both leap flags simultaneously).
These three methods belong to VehicleTimeStatus and encode VehicleTime-domain
semantics. ClockStatus<FlagEnumT> itself exposes only generic bit-manipulation
(IsFlagActive, IsAnyOfFlagsActive, AddFlag) and the domain-specific
PrintTo() specialization.
VT2 — Initialization and readiness check#
VehicleTime requires an explicit Init() call to open the IPC channel to the
TimeDaemon before any time data becomes available. Until Init() returns true,
Now() returns a snapshot with no flags set (IsConsistent() returns false) and IsAvailable() returns
false.
After a successful Init(), IsAvailable() returns true immediately. The
non-blocking IsAvailable() probe and the blocking WaitUntilAvailable() are useful
when Init() is retried on a background thread.
Simple startup (same thread):
#include "score/time/vehicle_time/src/vehicle_clock.h"
bool MyService::Startup()
{
auto clock = score::time::VehicleClock::GetInstance();
if (!clock.Init()) {
LOG_ERROR("VehicleTime: failed to open IPC channel");
return false;
}
auto snapshot = clock.Now();
// ...
return true;
}
Blocking wait when Init is retried from a background thread:
#include "score/time/vehicle_time/src/vehicle_clock.h"
#include <score/stop_token.hpp>
#include <chrono>
void MyService::WaitForClock(const score::cpp::stop_token& stop)
{
auto clock = score::time::VehicleClock::GetInstance();
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds{30};
if (!clock.WaitUntilAvailable(stop, deadline)) {
LOG_ERROR("VehicleTime did not become available within 30 s");
return;
}
auto snapshot = clock.Now();
// ...
}
Note
Init(), IsAvailable(), and WaitUntilAvailable() are only available on
clock domains that require explicit initialisation (currently VehicleTime).
Calling them on HighResSteadyTime, SteadyClock, or SystemClock is a compile
error — those clocks are always ready.
VT3 — Async PTP protocol data subscription#
VehicleTime exposes two PTP protocol data callbacks, intended primarily for
diagnostics and PTP data sanity checks:
TimeSlaveSyncData<VehicleTime>— fired on each PTP Sync/Follow_Up message pair; carries the offset, rate correction, and raw timestamps computed by the TimeSlave.PDelayMeasurementData<VehicleTime>— fired when a peer-delay measurement cycle completes; carries the measured peer delay and associated timestamps.
Warning
Both PTP data callbacks (TimeSlaveSyncData and PDelayMeasurementData) are
not yet delivered. Calling Subscribe<...>() compiles and runs without error,
but the registered callbacks will never be invoked. Delivery will be wired from a
dedicated background thread in a future change.
#include "score/time/vehicle_time/src/vehicle_clock.h"
#include "score/time/ptp/src/time_slave_sync_data.h"
#include "score/time/ptp/src/pdelay_measurement_data.h"
void MyDiagHandler::RegisterCallbacks()
{
auto clock = score::time::VehicleClock::GetInstance();
clock.Subscribe<score::time::TimeSlaveSyncData<score::time::VehicleTime>>(
[this](const auto& data) { OnTimeSyncData(data); });
clock.Subscribe<score::time::PDelayMeasurementData<score::time::VehicleTime>>(
[this](const auto& data) { OnPDelayData(data); });
}
void MyDiagHandler::Shutdown()
{
auto clock = score::time::VehicleClock::GetInstance();
clock.Unsubscribe<score::time::TimeSlaveSyncData<score::time::VehicleTime>>();
clock.Unsubscribe<score::time::PDelayMeasurementData<score::time::VehicleTime>>();
}
Warning
Callbacks are invoked on the backend thread — the callback implementation must be thread-safe.
VT4 — Synchronization status subscription#
Subscribe to VehicleTimeStatus changes to react when the clock synchronization state
changes — for example, when the timebase becomes synchronized and is ready to use, when a
timeout occurs, or when a large time leap is applied. This is the primary mechanism for
application components to know that VehicleTime is reliable and may be safely read.
Unlike the PTP protocol data callbacks in VT3, VehicleTimeStatus carries no protocol
internals. It delivers the same status value already available via Now().Status(),
but pushed proactively on every change rather than polled per call.
The callback fires unconditionally on the first PTP status update received after registration, and subsequently only when the flag set changes. Rate deviation is excluded from the comparison.
Warning
The VehicleTimeStatus callback is not yet delivered. Calling
Subscribe<VehicleTimeStatus>() compiles and runs without error, but the registered
callback will never be invoked. Delivery will be wired from a dedicated background
thread in a future change.
#include "score/time/vehicle_time/src/vehicle_clock.h"
void MyService::WatchClockReadiness()
{
auto clock = score::time::VehicleClock::GetInstance();
clock.Subscribe<score::time::VehicleTimeStatus>(
[this](const score::time::VehicleTimeStatus& status) {
if (status.IsReliable()) {
OnClockReady();
} else if (status.HasBeenSynchronized()) {
OnClockDegraded();
} else {
OnClockUnavailable();
}
});
}
void MyService::Shutdown()
{
auto clock = score::time::VehicleClock::GetInstance();
clock.Unsubscribe<score::time::VehicleTimeStatus>();
}
Warning
Callbacks are invoked on the backend thread — the callback implementation must be thread-safe.
VT5 — Status flag inspection#
When mapping VehicleTime status to diagnostic outputs such as DTC bitmasks, use
IsFlagActive(flag) with the VehicleTime::StatusFlag enum to access individual
bits. For the higher-level reliability predicates (IsReliable(),
HasBeenSynchronized()), see the status flag table and method descriptions in VT1.
#include "score/time/vehicle_time/src/vehicle_clock.h"
#include <map>
using SvtFlag = score::time::VehicleTime::StatusFlag;
static const std::map<SvtFlag, uint8_t> kDiagBitMap = {
{SvtFlag::kSynchronized, 0x01U},
{SvtFlag::kTimeOut, 0x02U},
{SvtFlag::kTimeLeapFuture, 0x04U},
{SvtFlag::kTimeLeapPast, 0x08U},
{SvtFlag::kUnknown, 0x80U},
};
uint8_t BuildDiagByte(const score::time::VehicleTimeStatus& status)
{
uint8_t result{0U};
for (const auto& entry : kDiagBitMap) {
if (status.IsFlagActive(entry.first)) {
result |= entry.second;
}
}
return result;
}
HighResSteadyTime#
HT1 — Time polling#
Measure a short code-path latency or compute a tight deadline where call overhead matters.
HighResSteadyClock avoids a kernel call on QNX by reading the hardware cycle counter
directly — the same Now() snapshot pattern used for all clock domains, with no status
check required.
#include "score/time/high_res_steady_time/src/high_res_steady_clock.h"
#include <chrono>
void MyValidator::CheckDeadline()
{
auto hirs = score::time::HighResSteadyClock::GetInstance();
const auto deadline = hirs.Now().TimePoint() + std::chrono::seconds{3};
// ... do work ...
if (hirs.Now().TimePoint() > deadline) {
HandleDeadlineExceeded();
}
}
SteadyClock#
ST1 — Time polling#
Measure elapsed time between two points, or derive a deadline, using a clock that is guaranteed never to go backward regardless of external time adjustments.
#include "score/time/steady_time/src/steady_clock.h"
void MyComponent::MeasureElapsed()
{
auto clock = score::time::SteadyClock::GetInstance();
const auto start = clock.Now().TimePoint();
// ... do work ...
const auto elapsed = clock.Now().TimePoint() - start;
}
SystemClock#
SC1 — Time polling#
Record a wall-clock timestamp for logging or audit trails where the absolute calendar
time matters. Do not use SystemClock for elapsed time or timeouts — the timepoint
may jump.
#include "score/time/system_time/src/system_clock.h"
#include <chrono>
void MyLogger::LogEvent()
{
auto clock = score::time::SystemClock::GetInstance();
const auto wall_time = clock.Now().TimePoint();
const auto t = std::chrono::system_clock::to_time_t(wall_time);
LOG_INFO("Event at: {}", std::ctime(&t));
}
Testing (all domains)#
Both test utilities work with any clock domain. Choose based on how the SUT obtains the clock:
Utility |
When to use |
|---|---|
|
SUT calls |
|
SUT accepts |
T1 — ScopedClockOverride (scope-bound override)#
Warning
ScopedClockOverride modifies a process-wide singleton. Any cc_test target that
uses it must declare tags = ["exclusive", "unit"] in its Bazel BUILD file. Without
"exclusive", Bazel may run multiple tests in the same process shard in parallel, causing
one test’s mock to corrupt another test’s clock state and producing flaky failures.
cc_test(
name = "my_service_test",
srcs = ["my_service_test.cpp"],
tags = ["exclusive", "unit"],
deps = [...],
)
If the SUT receives the clock via constructor injection instead, use
ClockTestFactory (T2) — it does not touch the global singleton and
requires no special tag.
#include "score/time/vehicle_time/src/vehicle_clock.h"
#include "score/time/vehicle_time/src/vehicle_clock_backend_mock.h"
TEST(MyServiceTest, ReportsReliableTime)
{
auto mock = std::make_shared<score::time::VehicleClockBackendMock>();
EXPECT_CALL(*mock, Now()).WillOnce(Return(/* snapshot */));
const score::time::test_utils::ScopedClockOverride<score::time::VehicleTime> guard{mock};
MyService svc;
svc.DoSomething(); // calls VehicleClock::GetInstance() internally
}
T2 — ClockTestFactory (constructor injection)#
#include "score/time/vehicle_time/src/vehicle_clock.h"
#include "score/time/vehicle_time/src/vehicle_clock_backend_mock.h"
TEST(MyServiceTest, ReportsReliableTime)
{
auto mock = std::make_shared<score::time::VehicleClockBackendMock>();
EXPECT_CALL(*mock, Now()).WillOnce(Return(/* snapshot */));
const auto clock =
score::time::test_utils::ClockTestFactory<score::time::VehicleTime>::Make(mock);
MyService svc{clock};
svc.DoSomething();
}
Bazel dependencies#
Choose the target that matches your use case:
Target |
When to use |
|---|---|
|
Production binary — includes real PTP backend |
|
Unit test — |
|
Test utilities — |
|
Header-only, no backend — interface/type usage only; required when subscribing
to |
|
Production binary — HIRS steady clock |
|
Unit test — |
|
Header-only |
|
|
|
|
|
PTP notification data types ( |
Design decisions#
Single entry point — no factory classes#
Classical time APIs expose a factory or manager object that clients instantiate and
configure before reading time (e.g. TimeBaseManager tm; tm.GetCurrentTime(kVehicleBase)).
score::time removes that level of indirection: Clock<Tag>::GetInstance() is the
sole entry point, and the production backend is chosen at link time by the Bazel
alias target.
Compile-time domain selection over runtime integer selector#
score::time addresses the same problem domain as time-base management modules found
in automotive middleware stacks: reading a time snapshot, inspecting synchronization
quality flags, waiting for clock availability, and subscribing to synchronization events.
The key design upgrade over typical C-style automotive APIs is replacing the runtime
integer time-base selector with a compile-time ``Tag`` template parameter. This
gives full type-safety and zero runtime dispatch for time-domain selection: a component
that depends on Clock<HighResSteadyTime> simply cannot accidentally read
VehicleTime at runtime — the compiler enforces the distinction. All other
structural concepts (composite snapshot result, quality status flags, layered backend
hiding) follow the same principles as established automotive time synchronization
practice, expressed in modern C++.
Opacity of details/#
Virtual dispatch exists solely to enable GMock test doubles. The vtable is hidden inside
details/ — public headers never declare a virtual function. Clock<Tag> is a plain
value type. The *_mock.h headers are the only public headers permitted to include
details/ internals.
ClockSnapshot — immutable composite result#
Classical time APIs return a raw timestamp and require a separate call to retrieve the
synchronization status, or expose a struct with public mutable data members and
raw-integer constructors. ClockSnapshot<TimepointT, StatusT> is a simple
immutable two-field struct:
auto snap = VehicleClock::GetInstance().Now();
snap.TimePoint(); // std::chrono::time_point<VehicleTime, nanoseconds>
snap.Status(); // VehicleTimeStatus — returned by value
Generic code works for all clock domains:
template <typename Tag>
auto Age(score::time::Clock<Tag>& clk,
typename score::time::Clock<Tag>::time_point ref)
{
return clk.Now().TimePoint() - ref;
}
Subscribe<E> — uniform subscription API#
Classical event-callback APIs require a separate named setter and unsetter for each event
type (e.g. SetSyncDataCallback, UnsetSyncDataCallback, SetPDelayCallback,
UnsetPDelayCallback). Clock<Tag> exposes a single pair Subscribe<E>() /
Unsubscribe<E>() templated on the event type.
The SubscriptionHook<Tag, EventType> specialisation bridges to the named virtual methods
on the backend interface — which must remain non-template (C++ forbids virtual templates).
Extending with a new clock domain#
Adding a new time domain (e.g. SdatTime) requires only new files — no existing file
is modified:
Create
score/time/sdat_time/sdat_time.h— tag struct withDurationandTimepoint, and a domain-specificSdatTimeStatusstruct containing whatever metadata the backend needs to expose (flags viaClockStatus<FlagEnumT>, continuous fields, or both).Create
score/time/sdat_time/details/sdat_time_iface.h— pure-virtual backend interface.Create
score/time/sdat_time/details/sdat_prod_impl.cpp— production backend.Add
ClockTraits<SdatTime>specialisation inscore/time/sdat_time/sdat_clock.h.Create
score/time/sdat_time/sdat_clock_mock.h— GMock test double.Add
sdat_time,sdat_time_mock,interfacealiases inscore/time/sdat_time/BUILD.(If the new domain requires explicit initialisation) Add a full specialisation of
InitializationHook<SdatTime>insdat_clock.hsupplyingstatic bool CallInit(Backend&) noexcept. This makesClock<SdatTime>::Init()available at compile time without touching any existing files.(If the new domain requires readiness checking) Add a full specialisation of
AvailabilityHook<SdatTime>insdat_clock.hsupplyingstatic bool CallIsAvailable(const Backend&)andstatic bool CallWaitUntilAvailable(const Backend&, stop_token, time_point). This makesIsAvailable()andWaitUntilAvailable()available at compile time.