Quanta overhaul: calibration speed, accuracy, and more #19
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR is the sum of many different changes all centered around: how do we make
quanta
safer and faster to use?Safety in Rust almost always carries the meaning of memory safety, but in this case, we're talking about the safety of the data itself: whether it can be relied on, what invariants are provided by the crate to users, etc.
The changes are vast so we'll list them out below in general sections.
Data safety / monotonicity
Simply put,
quanta
previously had no check to ensure that a second measurement from the sameClock
didn't end up coming before the previous measurement. This was/is a small possibility because of the nature of the Time Stamp Counter -- which could be fiddled with at runtime by the OS/hypervisor/etc -- and so the right thing to do is check for this condition and prevent it.With this PR,
Clock::now
will not return a non-monotonic measurement (but it may return the last measurement, so a delta of zero, but never a negative delta) and similarly. forClock::delta
, we'll never allow a negative delta.All methods that return raw values --
raw
,start
, andend
-- have no monotonicity guarantees themselves. If a user wants to useClock::scaled
, they're assuming the (small, but potential) risk of getting a non-monotonic conversion, butClock::delta
can still protect them.Data correctness / calibration
quanta
has always run a calibration loop as part of creating a newClock
. This calibration loop traditionally spun for one second before computing the result and moving on, which I didn't believe was high enough to originally warrant change. Catching a glimpse into howquanta
is used in the wild, it's clear that people are not OK withClock::new
taking a whole second, inexplicably. We can do better.Building off the work of the Linux kernel, we've redesigned the calibration loop such that it will do its work incrementally, terminating the loop if it believes it has a stable calibration. It will also set a deadline for itself which is much shorter than the previous deadline: 200ms vs 1 second. In practice, most calibration loops will finish in milliseconds, or tens of milliseconds. Given that computers are noisy, however, I've seen it also hit the deadline in testing: both cases still had a solid calibration. :)
Our calibration correctness/accuracy is computed: we do small chunks of work, adjust our overall reference/source ratios, and then take a measurement from each, seeing how far they diverge. Once this value (the mean, as well as the error) is suitably low -- we aim for 10ns or less divergence -- then we can be confident we're done.
A side note: a person may look at "10ns or less" and think: but isn't
quanta
supposed to give me nanosecond resolution? Will all my measurements be limited to 10ns resolution now? Nope! This is simply the difference between the reference and source clocks, so while the reference and source clock may be 10ns out of sync, nanosecond resolution is still present.This is also a fun rabbit hole to go down, because ultimately, these clock sources are all ticking at different rates, with different underlying clocks, some physical, some virtual, and it's quite the song and dance if you really wanted them all synced to each other. Even the latency of the methods themselves means you're always reading an old value!
We've punted on doing any sort of reading of the TSC frequency from the CPU directly because I don't have a good way to test it, and calibration should still be very very accurate.
Calibration re-use
Callers previously had no way to reuse a calibration, which means redoing the calibration work needlessly.
Clock::new
will now use a shared calibration -- guarded by astd::sync::Once
-based cell -- in order. to speed up creating new clocks and to avoid needing to expose the calibration itself and force callers to pass it around. Cloning an existingClock
will still work correctly, using the same calibration.Alright... that's a lot in one go, but suffice to say, there's a lot of changes here, and we're still working through the code itself, but will have to spend a good chunk of time ensuring that the documentation reflects the new behavior and features, as well as writing tests to cover more of these corner cases.
Ultimately, while
quanta
may have been created with the mindset of "fast, fast, fast!", what we really owe of users is "fast, fast, fast and correct!".Fixes #15.
Fixes #16.
Fixes #17.