Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quanta overhaul: calibration speed, accuracy, and more #19

Merged
merged 5 commits into from
May 26, 2020

Conversation

tobz
Copy link
Member

@tobz tobz commented May 24, 2020

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 same Clock 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. for Clock::delta, we'll never allow a negative delta.

All methods that return raw values -- raw, start, and end -- have no monotonicity guarantees themselves. If a user wants to use Clock::scaled, they're assuming the (small, but potential) risk of getting a non-monotonic conversion, but Clock::delta can still protect them.

Data correctness / calibration

quanta has always run a calibration loop as part of creating a new Clock. 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 how quanta is used in the wild, it's clear that people are not OK with Clock::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 a std::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 existing Clock 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.

@tobz tobz marked this pull request as draft May 24, 2020 13:54
@tobz tobz force-pushed the shared_calibration branch from 37b32a3 to 083f57d Compare May 24, 2020 22:35
tobz added 3 commits May 25, 2020 00:36
we can't actually test this on GCE because of emulated cpuid leaf stuff
and we don't have access to recent enough intel hardware so let's just
go with calibration because we know it works
@tobz tobz marked this pull request as ready for review May 26, 2020 17:05
@tobz tobz merged commit 92b0e7c into master May 26, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
1 participant