Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force evaluation X times per second?

Tags:

haskell

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

like image 566
James Strieter Avatar asked May 17 '19 17:05

James Strieter


1 Answers

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.

like image 109
Thomas M. DuBuisson Avatar answered Nov 10 '22 14:11

Thomas M. DuBuisson