I need a real time event loop that does something like this:
myEventLoop = do
businessLogic x0 x1 x2 ... xN
sleep 0.01
myEventLoop
I know about the eventloop package, but it seems to geared more towards web applications, and what I need is real time signal processing. Let's say the sample rate is 100 Hz. The sample rate doesn't need be super precise; if it's only 98 Hz once in awhile that's not a big deal. But I never want it to deviate more than 5%. For the moment we're only talking about GHC executing +/-5%. Ignore timing errors due to the OS.
My questions are: 1) Is this the kind of thing Haskell can do? I'm a bit concerned about the +/- 5% part. 2) Is the sleep part inefficient? Or can other Haskell threads occupy the same OS thread while myEventLoop is executing the sleep part? 3) Assuming sleeping is a good idea, hard coding the sleep time most likely is not. I can read the time stamp counter (TSC) since the program is running on an x86 architecture, but that approach could be problematic if GHC decides to chop up the program into microthreads, sending some to 1 CPU core and others to a different CPU core, as different cores can have different values for TSC. Is this a thing I should be worried about, and if so, what is a better approach?
Thanks in advance
You can try but I'm not optimistic. The GC and RTS will impact you in a significant negative manner but it's not possible for me to say if you'll care.
Eventloop is ... huge... personally I'd do something custom and light weight.
Don't use a "sleep then logic" setup because the "logic" part is not free and you'll end up with longer pauses than desired. My tests with "fork logic then sleep" show similarly bad behavior because sleep
won't start till the RTS schedules the parent thread again.
Custom solution using absolute times to avoid drift
A custom solution would look something like:
import Control.Concurrent
import Control.Monad
import Data.Time
periodic :: NominalDiffTime -> UTCTime -> IO () -> IO ()
periodic delayTime base operation =
do start <- getCurrentTime
let end = addUTCTime delayTime base
microSeconds = floor $ (1000000 :: Double) * realToFrac (diffUTCTime end start)
threadDelay microSeconds
void $ forkIO operation
periodic delayTime end operation
main :: IO ()
main =
do start <- getCurrentTime
forkIO $ periodic (1/98) start (getCurrentTime >>= print)
threadDelay 10000000 -- 10 seconds
This is simple and gets you really close to what any framework will offer unless you need some snazzy features like canceling events. You also get close to your desired period:
import Control.Concurrent
import Control.Monad
import Data.Time
periodic :: NominalDiffTime -> UTCTime -> IO () -> IO ()
periodic delayTime base operation =
do start <- getCurrentTime
let end = addUTCTime delayTime base
microSeconds = floor $ (1000000 :: Double) * realToFrac (diffUTCTime end start)
threadDelay microSeconds
void $ forkIO operation
periodic delayTime end operation
main :: IO ()
main =
do start <- getCurrentTime
forkIO $ periodic (1/98) start (getCurrentTime >>= print)
threadDelay 10000000 -- 10 seconds
Results in:
% ./y | wc -l
980
And you can analyze the printed times to get your confidence interval, but that will get worse as the load changes, heap grows, and GC needs to do more pauses.
Actual Answers
EDIT because you have actual questions.
1) Is this the kind of thing Haskell can do?
It is not really something Haskell is for - the GHC RTS is not geared for even soft real time work like audio.
2) Is the sleep part inefficient? Or can other Haskell threads occupy the same OS thread while myEventLoop is executing the sleep part?
Multiple Haskell threads can occupy the same OS thread, no issues.
3) Assuming sleeping is a good idea, hard coding the sleep time most likely is not. I can read the time stamp counter (TSC) since the program is running on an x86 architecture, but that approach could be problematic if GHC decides to chop up the program into microthreads, sending some to 1 CPU core and others to a different CPU core, as different cores can have different values for TSC. Is this a thing I should be worried about, and if so, what is a better approach?
Yes, you should worry about getting the Haskell thread moved around from core to core and OS thread to OS thread. The above is my attempt at a home grown approach. Eleven years ago I wrote control-event
(on hackage) to solve this same problem for me but my time constraints were less of an issue and needs a bit different. These days you can use the GHC event manager directly, though I'd avoid it because of the lower level API and it doesn't solve your underlying issue with the GC pauses.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With