Expand description
Performant cross-platform timing with goodies.
quanta provides a simple and fast API for measuring the current time and the duration between events. It does this
by providing a thin layer on top of native OS timing functions, or, if available, using high-speed timing features
(TSC, System Counter) found on modern CPUs.
§Design
Internally, quanta maintains the concept of two potential clock sources: a reference clock and a source clock.
The reference clock is provided by the OS, and always available. It is equivalent to what is provided by the standard library in terms of the underlying system calls being made. As it uses the native timing facilities provided by the operating system, we ultimately depend on the OS itself to give us a stable and correct value.
The source clock is a potential clock source based on the Time Stamp Counter or System Counter
feature found on modern x86-64 and AArch64 CPUs, respectively If these features are not present or are not reliable
enough, quanta will transparently utilize the reference clock instead.
Depending on the underlying processor(s) in the system, quanta will figure out the most accurate/efficient way to
calibrate the source clock to the reference clock in order to provide measurements scaled to wall clock time.
Details on TSC/System Counter support, and calibration, are detailed below.
§Features
Beyond simply taking measurements of the current time, quanta provides features for more easily working with
clocks, as well as being able to enhance performance further:
Clockcan be mocked for testing- globally accessible “recent” time with amortized overhead
§Mocked time
For any code that uses a Clock, a mocked version can be substituted. This allows for application authors to
control the time in tests, which allows simulating not only the normal passage of time but provides the ability to
warp time forwards and backwards in order to test corner cases in logic, etc. Creating a mocked clock can be
acheived with Clock::mock, and Mock contains more details on mock usage.
§Coarsely-updated, or recent, time
quanta also provides a “recent” time feature, which allows a slightly-delayed version of time to be provided to
callers, trading accuracy for speed of access. An upkeep thread is spawned, which is responsible for taking
measurements and updating the global recent time. Callers then can access the cached value by calling
Clock::recent. This interface can be 4-10x faster than directly calling Clock::now, even when TSC support is
available. As the upkeep thread is the only code updating the recent time, the accuracy of the value given to
callers is limited by how often the upkeep thread updates the time, thus the trade off between accuracy and speed of
access.
§Feature Flags
quanta comes with feature flags that enable convenient conversions to time types in other popular crates, such as:
prost- provides an implementation intoTimestampfromprost_types
§Platform Support
At a high level, quanta carries support for most major operating systems out of the box:
- Windows (
QueryPerformanceCounter) - macOS/OS X/iOS (
mach_absolute_time) - Linux/*BSD/Solaris (
clock_gettime)
These platforms are supported in the “reference” clock sense, and support for using TSC/System Counter as a clock source is more subtle, and explained below.
§WASM support
This library can be built for WASM targets, but in this case the resolution and accuracy of measurements can be
limited by the WASM environment. In particular, when running on the wasm32-unknown-unknown target in browsers,
quanta will use window.performance.now as a clock. This mean the accuracy is limited to milliseconds instead of
the usual nanoseconds on other targets. When running within a WASI environment (target wasm32-wasip1), the
accuracy of the clock depends on the VM implementation. On wasm32-unknown-emscripten, quanta uses Emscripten’s
POSIX-compatible monotonic clock.
§High-speed timing support
We support two primary mechanisms for a high-speed source clock: TSC on x86-64 platforms with SSE2, and System Counter on AArch64 platforms.
§Time Stamp Counter (x86-64)
For TSC, the processor must support either constant or nonstop/invariant TSC. This ensures that the TSC ticks at a constant rate which can be easily scaled. A caveat is that “constant” TSC doesn’t account for all possible power states (levels of power down or sleep that a CPU can enter to save power under light load, etc) and so a constant TSC can lead to drift in measurements over time, after they’ve been scaled to reference time.
This is a limitation of the TSC mode, as well as the nature of quanta not being able to know, as the OS would,
when a power state transition has happened, and thus compensate with a recalibration. Nonstop/invariant TSC does not
have this limitation and is stable over long periods of time.
Roughly speaking, the following list contains the beginning model/generation of processors where you should be able to expect having invariant TSC support:
- Intel Nehalem and newer for server-grade
- Intel Skylake and newer for desktop-grade
- VIA Centaur Nano and newer (circumstantial evidence here)
- AMD Phenom and newer
Ultimately, quanta will query CPUID information to determine if the processor has the required features to use the
TSC.
§System Counter (AArch64)
On ARMv8 and newer, the System Counter represents an “always-on device, which provides a fixed frequency incrementing system count. The system count value is broadcast to all the cores in the system, giving the cores a common view of the passage of time.”
This counter is readable through a user-mode system register that is always architecturally present, so compiling
against aarch64 is enough to utilize it.
One note is that prior to ARMv8.6-A, the counter frequency could vary between processors, and is anecdotally in the range of 1MHz to 50MHz, which means that the resolution of measurements might be as high as 1,000 nanoseconds. After ARMv8.6-A, and in ARMv9, the counter frequency is fixed at 1GHz, providing 1 nanosecond resolution.
§Calibration
As the TSC/System Counter don’t necessarily tick at reference scale – i.e. one tick isn’t always one nanosecond – we have to apply a scaling factor when converting from source to reference time scale to provide this. We acquire this scaling factor by repeatedly taking measurements from both the reference and source clocks, until we have a statistically-relevant measure of the average scaling factor. We do some additional work to convert this scaling factor into a power-of-two number that allows us to optimize the code, and thus reduce the generated instructions required to scale a TSC/System Counter value.
This calibration is stored globally and reused. However, the first Clock that is created in an application will
block for a small period of time as it runs this calibration loop. The time spent in the calibration loop is limited
to 200ms overall. In practice, quanta will reach a stable calibration quickly (usually 10-20ms, if not less) and
so this deadline is unlikely to be reached.
§Caveats
Utilizing the TSC/System Counter can be a tricky affair, and so here is a list of caveats that may or may not apply, and is in no way exhaustive:
- CPU hotplug behavior is undefined
- raw values may time warp
- measurements from the TSC/System Counter may drift past or behind the comparable reference clock
Another important caveat is that quanta does not track time across system suspends. Simply put, if a time
measurement (such as using Instant::now) is taken, and then the system is suspended, and
then another measurement is taken, the difference between those the two would not include the time the system was in
suspend.
Structs§
- Clock
- Unified clock for taking measurements.
- Handle
- Handle to a running upkeep thread.
- Instant
- A point-in-time wall-clock measurement.
- Mock
- Controllable time source for use in tests.
- Upkeep
- Ultra-low-overhead access to slightly-delayed time.
Enums§
- Error
- Errors thrown during the creation/spawning of the upkeep thread.
Traits§
- Into
Nanoseconds - Type which can be converted into a nanosecond representation.
Functions§
- set_
recent - Sets the global recent time.
- with_
clock - Sets this clock as the default for the duration of a closure.