packages feed

time-manager-0.5.2: System/TimeManager/Windows.hs

{-# LANGUAGE NumericUnderscores #-}

-- | A timer for Windows which does not depend on the I\/O manager.
--
--   @GHC.Event.Windows@ is WinIO's manager, so a timeout registered with
--   it is never run when the program uses the old Windows I\/O manager,
--   which is still the default.  This does the same job with one thread
--   and 'threadDelay' per timeout, which both managers drive.
module System.TimeManager.Windows (
    Manager,
    TimeoutKey,
    getSystemManager,
    registerTimeout,
    unregisterTimeout,
    updateTimeout,
) where

import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Word (Word64)
import GHC.Clock (getMonotonicTimeNSec)

-- | There is nothing to keep: each timeout is its own thread.
data Manager = Manager

getSystemManager :: IO Manager
getSystemManager = pure Manager

-- | A registered timeout.  The deadline is read by the thread rather
--   than captured, so that 'updateTimeout' does not have to replace it.
data TimeoutKey = TimeoutKey (IORef Word64) ThreadId

instance Eq TimeoutKey where
    TimeoutKey _ t1 == TimeoutKey _ t2 = t1 == t2

-- | Run the action once the given number of microseconds has passed.
registerTimeout :: Manager -> Int -> IO () -> IO TimeoutKey
registerTimeout _ us action = do
    ref <- newIORef =<< deadlineIn us
    tid <- forkIO $ wait ref
    pure $ TimeoutKey ref tid
  where
    wait ref = do
        deadline <- readIORef ref
        now <- getMonotonicTimeNSec
        if now >= deadline
            then action
            else do
                -- Round up, so that a deadline under a microsecond away
                -- is still waited for rather than busy looped on.
                threadDelay $ fromIntegral ((deadline - now + 999) `div` 1_000)
                wait ref

-- | Move the deadline the given number of microseconds into the future.
updateTimeout :: Manager -> TimeoutKey -> Int -> IO ()
updateTimeout _ (TimeoutKey ref _) us = writeIORef ref =<< deadlineIn us

unregisterTimeout :: Manager -> TimeoutKey -> IO ()
unregisterTimeout _ (TimeoutKey _ tid) = killThread tid

deadlineIn :: Int -> IO Word64
deadlineIn us = do
    now <- getMonotonicTimeNSec
    pure $ now + fromIntegral (max 0 us) * 1_000