summaryrefslogtreecommitdiffhomepage
path: root/src/Xmobar/App
diff options
context:
space:
mode:
Diffstat (limited to 'src/Xmobar/App')
-rw-r--r--src/Xmobar/App/Timer.hs122
1 files changed, 96 insertions, 26 deletions
diff --git a/src/Xmobar/App/Timer.hs b/src/Xmobar/App/Timer.hs
index d67695d..8464fb7 100644
--- a/src/Xmobar/App/Timer.hs
+++ b/src/Xmobar/App/Timer.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE LambdaCase #-}
------------------------------------------------------------------------------
-- |
-- Module: Xmobar.App.Timer
@@ -11,16 +12,23 @@
--
------------------------------------------------------------------------------
-module Xmobar.App.Timer (doEveryTenthSeconds, withTimer) where
+module Xmobar.App.Timer
+ ( doEveryTenthSeconds
+ , tenthSeconds
+ , withTimer
+ ) where
+import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (withAsync)
import Control.Concurrent.STM
-import Control.Exception (bracket, bracket_)
-import Control.Monad (forever, forM, forM_, guard)
+import Control.Exception
+import Control.Monad (forever, forM, guard)
+import Data.Foldable (foldrM)
import Data.IORef
import Data.Int (Int64)
import Data.Map (Map)
import qualified Data.Map as M
+import Data.Maybe (isJust)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Unique
import System.IO.Unsafe (unsafePerformIO)
@@ -29,7 +37,12 @@ newtype Timer = Timer (TVar Periods)
type Periods = Map Unique Period
-data Period = Period { rate :: Int64, next :: Int64, tick :: TMVar (TMVar ()) }
+data Tick = Tick (TMVar ()) | TimeOut
+
+data Period = Period { rate :: Int64, next :: Int64, tick :: TMVar Tick }
+
+data TimeOutException = TimeOutException deriving Show
+instance Exception TimeOutException
{-# NOINLINE timer #-}
timer :: IORef (Maybe Timer)
@@ -50,24 +63,48 @@ newPeriod r = do
-- | Perform a given action every N tenths of a second.
--
--- The timer is aligned with other timers to minimize the number of wakeups
--- and unnecessary redraws.
+-- The timer is aligned (coalesced) with other timers to minimize the number
+-- of wakeups and unnecessary redraws. If the action takes too long (one
+-- second or when the next timer is due), coalescing is disabled for it and it
+-- falls back to periodic sleep.
doEveryTenthSeconds :: Int -> IO () -> IO ()
doEveryTenthSeconds r action = do
Just t <- readIORef timer
- doEveryTenthSeconds' t r action
+ doEveryTenthSecondsCoalesced t r action `catch` \TimeOutException ->
+ doEveryTenthSecondsSleeping r action
-doEveryTenthSeconds' :: Timer -> Int -> IO () -> IO ()
-doEveryTenthSeconds' (Timer periodsVar) r action = do
+-- | Perform a given action every N tenths of a second,
+-- coalesce with other timers using a given Timer instance.
+doEveryTenthSecondsCoalesced :: Timer -> Int -> IO () -> IO ()
+doEveryTenthSecondsCoalesced (Timer periodsVar) r action = do
(u, p) <- newPeriod (fromIntegral r)
- bracket_ (push u p) (pop u) $ forever $
- bracket (atomically $ takeTMVar $ tick p)
- (\doneVar -> atomically $ putTMVar doneVar ())
- (const action)
+ bracket_ (push u p) (pop u) $ forever $ bracket (wait p) done $ const action
where
push u p = atomically $ modifyTVar periodsVar (M.insert u p)
pop u = atomically $ modifyTVar periodsVar (M.delete u)
+ wait p = atomically (takeTMVar $ tick p) >>= \case
+ Tick doneVar -> return doneVar
+ TimeOut -> throwIO TimeOutException
+ done doneVar = atomically $ putTMVar doneVar ()
+
+-- | Perform a given action every N tenths of a second,
+-- making no attempt to synchronize with other timers.
+doEveryTenthSecondsSleeping :: Int -> IO () -> IO ()
+doEveryTenthSecondsSleeping r action = go
+ where go = action >> tenthSeconds r >> go
+
+-- | Sleep for a given amount of tenths of a second.
+--
+-- (Work around the Int max bound: since threadDelay takes an Int, it
+-- is not possible to set a thread delay grater than about 45 minutes.
+-- With a little recursion we solve the problem.)
+tenthSeconds :: Int -> IO ()
+tenthSeconds s | s >= x = do threadDelay (x * 100000)
+ tenthSeconds (s - x)
+ | otherwise = threadDelay (s * 100000)
+ where x = (maxBound :: Int) `div` 100000
+
-- | Start the timer coordination thread.
withTimer :: (IO () -> IO ()) -> IO a -> IO a
withTimer pauseRefresh action = do
@@ -80,19 +117,25 @@ withTimer pauseRefresh action = do
timerLoop :: (IO () -> IO ()) -> TVar Periods -> IO ()
timerLoop pauseRefresh periodsVar = forever $ do
- t <- now
- toFire <- atomically $ do
+ tNow <- now
+ (toFire, tMaybeNext) <- atomically $ do
periods <- readTVar periodsVar
- writeTVar periodsVar (advanceTimers t periods)
- return (timersToFire t periods)
+ let toFire = timersToFire tNow periods
+ let periods' = advanceTimers tNow periods
+ let tMaybeNext = nextFireTime periods'
+ writeTVar periodsVar periods'
+ return (toFire, tMaybeNext)
pauseRefresh $ do
- -- Fire timers ...
- doneVars <- atomically $ forM toFire $ \p -> do
- doneVar <- newEmptyTMVar
- putTMVar (tick p) doneVar
- return doneVar
- -- ... and wait for them to avoid unnecessary redraws.
- atomically $ forM_ doneVars takeTMVar
+ -- To avoid multiple refreshes, pause refreshing for up to 1 second,
+ -- fire timers and wait for them to finish (update their text).
+ -- Those that need more time (e.g. weather monitors) will be dropped
+ -- from timer coalescing and will fall back to periodic sleep.
+ timeoutVar <- registerDelay $ case tMaybeNext of
+ Just tNext -> fromIntegral ((tNext - tNow) `max` 10) * 100000
+ Nothing -> 1000000
+ fired <- fireTimers toFire
+ timeouted <- waitForTimers timeoutVar fired
+ timeoutTimers timeouted periodsVar
delayUntilNextFire periodsVar
advanceTimers :: Int64 -> Periods -> Periods
@@ -101,14 +144,41 @@ advanceTimers t = M.map advance
advance p | next p <= t = p { next = t - t `mod` rate p + rate p }
| otherwise = p
-timersToFire :: Int64 -> Periods -> [Period]
-timersToFire t periods = [ p | p <- M.elems periods, next p <= t ]
+timersToFire :: Int64 -> Periods -> [(Unique, Period)]
+timersToFire t periods = [ (u, p) | (u, p) <- M.toList periods, next p <= t ]
nextFireTime :: Periods -> Maybe Int64
nextFireTime periods
| M.null periods = Nothing
| otherwise = Just $ minimum [ next p | p <- M.elems periods ]
+fireTimers :: [(Unique, Period)] -> IO [(Unique, TMVar ())]
+fireTimers toFire = atomically $ forM toFire $ \(u, p) -> do
+ doneVar <- newEmptyTMVar
+ putTMVar (tick p) (Tick doneVar)
+ return (u, doneVar)
+
+waitForTimers :: TVar Bool -> [(Unique, TMVar ())] -> IO [Unique]
+waitForTimers timeoutVar fired = atomically $ do
+ timeoutOver <- readTVar timeoutVar
+ dones <- forM fired $ \(u, doneVar) -> do
+ done <- isJust <$> tryReadTMVar doneVar
+ return (u, done)
+ guard $ timeoutOver || all snd dones
+ return [u | (u, False) <- dones]
+
+-- | Handle slow timers (drop and signal them to stop coalescing).
+timeoutTimers :: [Unique] -> TVar Periods -> IO ()
+timeoutTimers timers periodsVar = atomically $ do
+ periods <- readTVar periodsVar
+ periods' <- foldrM timeoutTimer periods timers
+ writeTVar periodsVar periods'
+
+timeoutTimer :: Unique -> Periods -> STM Periods
+timeoutTimer u periods = do
+ putTMVar (tick (periods M.! u)) TimeOut
+ return $ u `M.delete` periods
+
delayUntilNextFire :: TVar Periods -> IO ()
delayUntilNextFire periodsVar = do
tMaybeNext <- fmap nextFireTime $ readTVarIO periodsVar