I needed a very basic hybrid linear clock implementation for a project implementing MVCC. It gives me a perfectly incrementing clock that I can use to compare timestamps that can handle high load periods. It's not designed to synchronize across a network, just across threads.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU128};
use std::sync::atomic::Ordering::Relaxed;
use std::thread;
use std::thread::JoinHandle;
use std::time::{Duration, SystemTime};
/// How often the clock is synchronized with the source.
pub const TICK_FREQUENCY_IN_NS: u64 = 500;
pub struct HybridLogicalClock {
initial_clock: u128,
last_tick: Arc<AtomicU128>,
updater: JoinHandle<()>,
done: Arc<AtomicBool>,
}
#[allow(clippy::new_without_default)]
impl HybridLogicalClock {
pub fn new() -> Self {
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos();
let last_tick = Arc::new(AtomicU128::new(now));
let done = Arc::new(AtomicBool::new(false));
let last_tick_clone = last_tick.clone();
let done_clone = done.clone();
let updater = thread::spawn(move || {
while !done_clone.load(Relaxed) {
thread::sleep(Duration::from_nanos(TICK_FREQUENCY_IN_NS));
let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos();
let diff = now - last_tick_clone.load(Relaxed);
if diff == 0 {
continue;
}
last_tick_clone.fetch_add(diff, Relaxed);
}
});
Self {
initial_clock: now,
last_tick,
updater,
done,
}
}
#[inline]
pub fn time(&self) -> u128 {
self.last_tick.store(self.last_tick.load(Relaxed) + 1, Relaxed);
self.last_tick.load(Relaxed)
}
}
impl Drop for HybridLogicalClock {
fn drop(&mut self) {
self.done.store(true, Relaxed);
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use crate::hlc::HybridLogicalClock;
#[test]
fn test_time() {
let clock = HybridLogicalClock::new();
let mut last_time = 0;
for _ in 0..100 {
let now = clock.time();
assert_ne!(now, 0, "clock must never be zero");
assert!(now > last_time, "now must be greater than last");
}
}
}
It's pretty performant with criterion:
pub fn clock_gettime(c: &mut Criterion) {
let clock = HybridLogicalClock::new();
c.bench_function("HybridLogicalClock::time()", |b| b.iter(|| clock.time()));
}
produces (on my Mac M1 Pro)
Running benches/hlc.rs (target/release/deps/hlc-f01a21dc13fa8cc7)
HybridLogicalClock::time()
time: [2.5062 ns 2.5085 ns 2.5108 ns]
change: [-2.1275% -1.7493% -1.3941%] (p = 0.00 < 0.05)
Performance has improved.
Found 20 outliers among 100 measurements (20.00%)
6 (6.00%) low severe
5 (5.00%) low mild
3 (3.00%) high mild
6 (6.00%) high severe
I have hit the limit of my knowledge of Rust and memory atomics, so I'm happy with the average results of 2.5ns to fetch the "timestamp".
What are some ways that I can improve the performance of this implementation?