How to implement a steady ticker (if feasible)?

I'm developing a millisecond ticker to provide the timing for a polyrhythmic music sequencer. For this application, I think a steady sticker would be best. The priority is for the ticks to all occur after the same interval, as far as possible, rather than that, for example, after 600,000 1-millisecond ticks exactly 10 minutes elapsed time will has passed.

Rust has a SteadyClock. But I can't see how to get tick notifications out of it. Is there a way to get steady ticks that I have missed?

If not, my fallback option, which I have provisionally implemented, is Instant, of which there is a tokio version, which I'm using, and a std version. The documentation for Instant states that it is

A measurement of a monotonically nondecreasing clock. ... Instants are not guaranteed to be steady. In other words, each tick of the underlying clock might not be the same length (e.g. some seconds may be longer than others). An instant may jump forwards or experience time dilation (slow down or speed up), but it will never go backwards.

I don't understand how an instant could go backwards. Regardless of that, if it is not feasible to implement a steady ticker, Instant should be adequate. My tests show that it performs the same as JUCE's C++ class HighResolutionTimer, which is designed for music and I've used extensively.

2 Likes

Tickers are repeated timers, and timers generally require either platform support or busy waiting, burning a CPU core checking the current time over and over.

Thankfully sleep (both std and tokio) is generally millisecond accurate now consistently (it used to be much less accurate by default on Windows for power reasons) so that's normally fine to use by itself, though be aware if you actually want really high consistency you'll want to undershoot the next tick by a millisecond or so and then busy wait (check time in a loop) as sleep will only schedule your code to run and won't necessarily interrupt any other code running. You may need to increase the thread priority too, but it's not a magic "higher is faster" button, tune that last if needed as it can cause issues on a loaded system.

In order to handle compute time between ticks, keep a "next tick" Instant that you increment each time rather than trying to sleep the same period, which means you also need to consider what happens if you over schedule; that is when you're running code for longer than the tick rate. Generally I feel it's best to allow it to fall behind up to one additional tick, then to simply run ticks continuously but it depends on context.

I'm sure there's crates that handle at least some of this for you, but a very quick search didn't find much that was all that promising.

3 Likes

Thanks, @simonbuchan. I'll investigate and report back.

My mistake, sorry. My code currently uses Interval, not Instant. So the warning in the documentation that Instant is not steady is not relevant. I need to do some more in depth testing to see whether I need to improve anything.

After measuring the actual intervals between ticks with various specified intervals, I conclude that Interval is adequate for what I need. I found that the absolute differences between expected and measured intervals increased little as I varied the specified interval duration. So I think the differences must be mostly due to test artefacts.

I think I should be able to get more consistent tick durations if I were to set MissedTickBehavior to Delay. That would be more like a steady clock. However, on due consideration, I've decided that it's best for my musical purpose to leave MissedTickBehavior at its default, Burst. That will speed up ticks, if necessary, to keep the elapsed time of all ticks as expected while still notifying every tick. That would be more like a system clock. It would account for the zero-millisecond measurements you can see for two of the ticks in the data below where a 10 millisecond interval was specified.

    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.
    Expected 1 milliseconds; actual 1 milliseconds.

    Expected 10 milliseconds; actual 19 milliseconds.
    Expected 10 milliseconds; actual 0 milliseconds.
    Expected 10 milliseconds; actual 16 milliseconds.
    Expected 10 milliseconds; actual 15 milliseconds.
    Expected 10 milliseconds; actual 0 milliseconds.
    Expected 10 milliseconds; actual 16 milliseconds.
    Expected 10 milliseconds; actual 16 milliseconds.
    Expected 10 milliseconds; actual 15 milliseconds.
    Expected 10 milliseconds; actual 15 milliseconds.
    Expected 10 milliseconds; actual 0 milliseconds.

    Expected 100 milliseconds; actual 14 milliseconds.
    Expected 100 milliseconds; actual 92 milliseconds.
    Expected 100 milliseconds; actual 100 milliseconds.
    Expected 100 milliseconds; actual 99 milliseconds.
    Expected 100 milliseconds; actual 95 milliseconds.
    Expected 100 milliseconds; actual 111 milliseconds.
    Expected 100 milliseconds; actual 95 milliseconds.
    Expected 100 milliseconds; actual 95 milliseconds.
    Expected 100 milliseconds; actual 99 milliseconds.
    Expected 100 milliseconds; actual 96 milliseconds.

    Expected 1000 milliseconds; actual 1002 milliseconds.
    Expected 1000 milliseconds; actual 990 milliseconds.
    Expected 1000 milliseconds; actual 1005 milliseconds.
    Expected 1000 milliseconds; actual 1007 milliseconds.
    Expected 1000 milliseconds; actual 988 milliseconds.
    Expected 1000 milliseconds; actual 1004 milliseconds.
    Expected 1000 milliseconds; actual 991 milliseconds.
    Expected 1000 milliseconds; actual 1004 milliseconds.
    Expected 1000 milliseconds; actual 1006 milliseconds.
    Expected 1000 milliseconds; actual 991 milliseconds.

    Expected 10000 milliseconds; actual 9997 milliseconds.
    Expected 10000 milliseconds; actual 10009 milliseconds.
    Expected 10000 milliseconds; actual 9997 milliseconds.
    Expected 10000 milliseconds; actual 9993 milliseconds.
    Expected 10000 milliseconds; actual 10010 milliseconds.
    Expected 10000 milliseconds; actual 9996 milliseconds.
    Expected 10000 milliseconds; actual 10001 milliseconds.
    Expected 10000 milliseconds; actual 10000 milliseconds.
    Expected 10000 milliseconds; actual 10002 milliseconds.
    Expected 10000 milliseconds; actual 9991 milliseconds.

Yeash that's still pretty rough for audio above 1ms.

It looks like above that the timer is using the default Windows 16ms timer resolution? I assume Rust is using timeBeginPeriod below a certain frequency (less than 10?) That implies that if you have one 1ms timer going, the other timers will be 1ms accurate, which is pretty lame.

@simonbuchan Test results on subsequent days were poor, even for the 1-second tick interval. So I've abandoned using Interval as the tick source and instead taken your advice to use sleep.

I'm very happy with the test results shown below, which were run in Windows. They show that the ticker is now very steady, though the first one or two ticks can be shaky. Elapsed times after many ticks are less than expected for short tick intervals but about right for tick intervals of around 100 milliseconds or more. I'll post my code in a separate reply.

MeasureTickIntervals: testing 1-millisecond tick interval. Sleeping for 101 milliseconds.
Tick interval milliseconds: expected 1; actual 2.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
Tick interval milliseconds: expected 1; actual 1.
************************************************
Total tick count: 65.
Elapsed milliseconds: expected total tick count 65 * interval 1 = 65; measured 102.
************************************************
MeasureTickIntervals: testing 10-millisecond tick interval. Sleeping for 501 milliseconds.
Tick interval milliseconds: expected 10; actual 1.
Tick interval milliseconds: expected 10; actual 8.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
Tick interval milliseconds: expected 10; actual 10.
************************************************
Total tick count: 50.
Elapsed milliseconds: expected total tick count 50 * interval 10 = 500; measured 510.
************************************************
MeasureTickIntervals: testing 100-millisecond tick interval. Sleeping for 3001 milliseconds.
Tick interval milliseconds: expected 100; actual 9.
Tick interval milliseconds: expected 100; actual 91.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 99.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
Tick interval milliseconds: expected 100; actual 100.
************************************************
Total tick count: 30.
Elapsed milliseconds: expected total tick count 30 * interval 100 = 3000; measured 3001.
************************************************
MeasureTickIntervals: testing 1000-millisecond tick interval. Sleeping for 30001 milliseconds.
Tick interval milliseconds: expected 1000; actual 10.
Tick interval milliseconds: expected 1000; actual 990.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
Tick interval milliseconds: expected 1000; actual 1000.
************************************************
Total tick count: 30.
Elapsed milliseconds: expected total tick count 30 * interval 1000 = 30000; measured 30010.
************************************************

Here's the code for my steady ticker:

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration};

/// A ticker that calls a callback on ticking and can be started and stopped.
pub struct Ticker {
    interval: Duration,
    running: Arc<AtomicBool>,
}

impl Ticker {
    pub fn new(interval: Duration) -> Self {
        Self {
            interval,
            running: Arc::new(AtomicBool::new(false)),
        }
    }

    /// Starts the ticker with a callback
    pub fn start<F>(&mut self, callback: F)
    where
        F: Fn() + Send + Clone + 'static,
    {
        let running = self.running.clone();
        let interval = self.interval;
        running.store(true, Ordering::SeqCst);
        thread::spawn(move || {
            while running.load(Ordering::SeqCst) {
                thread::sleep(interval);
                let callback_clone = callback.clone();
                thread::spawn(move || { callback_clone() });
            }
        });
    }

    /// Stops the ticker
    pub fn stop(&self) {
        self.running.store(false, Ordering::SeqCst);
    }
}
1 Like

You'd probably want to use a thread pool for the tasks like rayon, as spawning a thread is pretty heavy, but you're also opening yourself up to the spawned task taking longer than your timer, which means you just keep stacking up threads until the whole thing breaks horribly.

Running the task inline is a very reliable way to avoid that, but you need to as mentioned add the interval to the next scheduled time to handle the task execution time, and handle it running behind the current time.

One simplified approach if you want to do that is the following (which you would need to run in a thread of course):

Otherwise, you can use a custom thread pool and limit the number of threads using ThreadPoolBuilder::num_threads

1 Like

I should explain the context in which my ticker will be used. I'm developing a .Net MIDI polyrhythmic sequencing application in C#. I need a high precision timer that can tick steadily at 1-millisecond intervals. .Net has nothing that can do this: its sleeping and waiting utilities are very slow and variable when set to such short durations.

So I'm developing a tiny Rust library whose only requirement is to expose a steady ticker to C# via Rust's Foreign Function Interface (FFI). The C# application needs to be able to start the ticker, specifying the tick interval in milliseconds, receive an on-tick callback on a different thread, and stop the ticker.

I have 23 years C# experience, while this is my first attempt to code Rust to do something useful, having just completed the 100 Exercises to Learn Rust self-training course. So I really appreciate your help!

I now have my current requirements adequately implemented. The tests whose results I've shown were written in C#, calling Rust functions via the FFI. As you point out though, I need to consider possible problems and improvements.

So I've looked at the two possibilities you suggest. The Rust library knows nothing about what other work the C# application is doing. It just needs to asynchronously notify the C# application of each tick. So, though I like the idea of catching up the ticks in the event of a delay, I don't see how I can integrate the way this is done in your simplified code example into my library. My current thinking is that it's not necessary to do so. I'm open to suggestions though.

The polyrhythmic sequencing application supports multiple concurrent voices, each playing a different rhythm. But currently there is only a requirement for a single ticker, with a 1-millisecond interval. At each 1-millisecond interval, the application works out what, if anything needs to be done with each voice. A tick interval shorter than 1 millisecond would actually be very useful with this approach! However, I've established that, at least in Windows, 1 millisecond is the shortest possible.

So a better approach might be to have a separate ticker for each voice, perhaps with each ticker's interval continually varying. In that case, the 1-second minimum would not be a problem. If I decide to implement this approach, I can see that the thread pool provided by Rayon might be useful.

Yeah, you need to be careful about FFI, it often adds subtle concerns at the border you don't need to think about otherwise - hopefully the library you're using handles all that reliably! That said, while I haven't used any Rust/.NET FFI yet, you generally are able to make blocking FFI calls in both directions, so you should be able to use a blocking loop in theory?

In any case, you could also check in the callback if it's already running and bail out, presumably that won't take anywhere near 1ms!

If you really want to get very high accuracy, you can spin, but that effectively ensures 100% of one CPU core usage if you can't sleep away any of it. The best results are a fiddly mix of all the APIs mentioned in this thread, which is really annoying. Here's an example I found while searching for other info of how fiddly this can get for just Windows: The perfect Sleep() function

Fortunately, it seems a decent amount of that is implemented by spin_sleep, though as you can see from the API that's still plenty of details!

Apparently Linux can precisely sleep down to nanoseconds... when it's configured. Fun.

3 Likes

Thanks for the excellent suggestion! I've gone with spin_sleep with the default native accuracy, like this:

spin_sleep::sleep(interval);

That is perfectly steady (after the usual shaky first two ticks) for a 1-millisecond interval, which was not quite achieved by thread::sleep. That's what I want.

I also tried SpinWait, like this:

let spinner = SpinWait::new();
[...]
    let interval_start = Instant::now();
    spinner.spin_until(|| interval_start.elapsed() >= interval);

That's perfect for the elapsed/system time of multiple ticks. But it's not steady.

Exactly. The C# callback is very quick. When it has any heavy lifting to do, it does it asynchronously. So I should not get stacked up tick threads.

My code needs to be able run on Linux and macOS and well as Windows. Even if I wanted to, it would not be feasible for me to make a custom procedure for each operating system: I've only got Windows myself!

Yeah I mostly keep mentioning it because if it does start happening later the results are nearly as catastrophic as you can get while still being "correct". You're almost always better off even failing loudly than repeatedly stacking up tasks.

Yeah, otherwise I'd be talking about specific high resolution APIs and recent CPU instructions (different for Intel and AMD of course :roll_eyes:)... it's all way overkill if you're not right on the very edge counting nanoseconds.

1 Like

@simonbuchan Indeed no, nanosecond accuracy is not needed in music sequencing applications, for which I primarily designed the ticker. After all, human musicians can't achieve anything near that.

Following your tip, I am now using Rayon for spawning.

My ticker, together with its FFI and C# wrappers, are now available on GitHub at Millisecond Ticker: High Resolution Ticker for .Net using Rust. Here is the current code of the ticker module. Validation is minimal, as that is taken care of more fully in the C# class.

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

/// A steady ticker that asynchronously calls a callback on ticking
/// and can be started and stopped.
/// Timer: spin_sleep::sleep
///     Benchmark test with 1-ms interval specified:
///         Average actual interval: 1.00196 ms
///         CPU usage: 2-3%
/// Other steady timers tested and commented out:
/// SteadyClock with SpinWait
///     Benchmark test with 1-ms interval specified:
///         Average actual interval: 1.00175 ms
///         CPU usage: 4-9% (Uses the whole of one CPU core.)
/// std::thread::sleep
///     Benchmark test with 1-ms interval specified:
///         Average actual interval: 1.51934 ms
///         CPU usage: 0-3%
/// Benchmark tests were run on a Windows PC with
/// an AMD Ryzen 9 5900X 12-Core processor.
pub struct Ticker {
    interval: Duration,
    running: Arc<AtomicBool>,
}

impl Ticker {
    pub fn new(interval: Duration) -> Self {
        if interval.as_millis() < 1 {
            panic!("Interval must be at least 1 millisecond.");
        }
        Self {
            interval,
            running: Arc::new(AtomicBool::new(false)),
        }
    }

    /// Starts the ticker with a callback.
    pub fn start<F>(&mut self, callback: F)
    where
        F: Fn() + Send + Clone + 'static,
    {
        let running = self.running.clone();
        if running.load(Ordering::SeqCst) {
            panic!("The ticker is already running.");
        }
        let interval = self.interval;
        // let spinner = SpinWait::new();
        running.store(true, Ordering::SeqCst);
        // We cannot use std::thread::spawn here. If an Avalonia C# application is run
        // in an IDE (JetBrains Rider or Visual Studio), Rust panics when attempting to spawn,
        // with this error message:
        //     failed to spawn thread: Os { code: 5, kind: PermissionDenied, message: "Access is denied." }
        // Rayon::spawn does not have this problem, as it uses a thread pool that Rayon has created
        // in advance.
        // The type of spawn used has not made any measurable difference to performance.
        rayon::spawn(move || {
            while running.load(Ordering::SeqCst) {
                // std::thread::sleep(interval);
                spin_sleep::sleep(interval);
                // let interval_start = SteadyClock::now();
                // spinner.spin_until(|| (SteadyClock::now() - interval_start) >= interval);
                let callback_clone = callback.clone();
                rayon::spawn(move || { callback_clone() });
            }
        });
    }

    /// Stops the ticker.
    pub fn stop(&self) {
        self.running.store(false, Ordering::SeqCst);
    }
}
1 Like

I assume this would be with a RT kernel and SCHED_FIFO or SCHED_DEADLINE? I don't think a normal kernel is going to give you that.

Apparently, though I think even a more "normal" config has something more like microseconds, but can be stripped down to near the 16 millisecond accuracy of Windows.

Just another one of those great "who knows what you're going to get" things that makes Linux actually 30 different operating systems in a trenchcoat! :face_savoring_food:

That's very flexibility is what allows Linux to scale from embedded systems, to phones, to desktops and to servers. Usually mainstream desktop Linux distros is configured reasonably similar though.

Oh yeah, but it does mean "this runs on Linux" by itself pretty non-descriptive without a decent bit of context. Treating it as just "the third operating system" is a bad idea for many reasons (not least because it's actually the first by most sensible measures)

2 Likes

This sounds like the assurance I made when I had to synchronise two clocks. The local clock was accurate to 150ppm (standard for PC clocks), whereas the data was coming in accurate to 20ppm. It made a difference of 20 minutes over a 3,000 hour test. The customer noticed there was 20 minutes' worth of data missing and was not happy.
The data was only coming in every approximately 90ms. I couldn't use it directly. The solution was to create a phase-locked loop that calculated the accurate time as an offset and multiplier of the local time. It would never go backwards, but might stretch or compress seconds; exactly the same assurance that Instant gives.

1 Like

You can run into it even locally where media playback likes to get locked to whatever the output (ie. monitor or DAC) clock is. It's pretty easy to assume 60Hz display and 48kHz audio means you need exactly 800 samples per frame, but that's not actually guaranteed depending on platform. (or specific hardware installed)