timer-wheel 1.0.0 → 1.0.0.1
raw patch · 10 files changed
+263/−226 lines, 10 filesdep ~basedep ~randomdep ~tasty-bench
Dependency ranges changed: base, random, tasty-bench
Files
- CHANGELOG.md +12/−7
- LICENSE +1/−1
- src/TimerWheel.hs +68/−175
- src/TimerWheel/Internal/Alarm.hs +12/−0
- src/TimerWheel/Internal/AlarmBuckets.hs +102/−0
- src/TimerWheel/Internal/Bucket.hs +11/−11
- src/TimerWheel/Internal/Prelude.hs +8/−7
- src/TimerWheel/Internal/Timer.hs +29/−0
- test/Main.hs +1/−1
- timer-wheel.cabal +19/−24
CHANGELOG.md view
@@ -1,5 +1,10 @@-## [1.0.0] - 2023-10-10+## [1.0.0.1] - May 16, 2024 +- Fix `term-variable-capture` warnings+- Require at least GHC 9.2++## [1.0.0] - October 10, 2023+ - Add `count`, which returns the number of timers in a timer wheel - Add `Seconds` type alias for readability - Add `Timer` newtype for readability@@ -12,11 +17,11 @@ - Improve the resolution of timers from microseconds to nanoseconds - Simplify and optimize internals -## [0.4.0.1] - 2022-11-05+## [0.4.0.1] - November 5, 2022 - Fix inaccurate haddock on `recurring` -## [0.4.0] - 2022-11-05+## [0.4.0] - November 5, 2022 - Add `create` - Rename `Data.TimerWheel` to `TimerWheel`@@ -24,7 +29,7 @@ - Treat negative delays as 0 - Drop support for GHC < 8.6 -## [0.3.0] - 2020-06-18+## [0.3.0] - June 18, 2020 - Add `with` - Add support for GHC 8.8, GHC 8.10@@ -38,11 +43,11 @@ - Remove `InvalidTimerWheelConfig` exception. `error` is used instead - Remove support for GHC < 8.6 -## [0.2.0.1] - 2019-05-19+## [0.2.0.1] - May 19, 2019 - Swap out `ghc-prim` and `primitive` for `vector` -## [0.2.0] - 2019-02-03+## [0.2.0] - February 3, 2019 - Add `destroy` function, for reaping the background thread - Add `recurring_` function@@ -57,6 +62,6 @@ - Rename `new` to `create` - Make recurring timers more accurate -## [0.1.0] - 2018-07-18+## [0.1.0] - July 18, 2018 - Initial release
LICENSE view
@@ -1,4 +1,4 @@-Copyright 2018-2023 Mitchell Rosen, Travis Staton+Copyright 2018-2024 Mitchell Dalvi Rosen, Travis Staton Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
src/TimerWheel.hs view
@@ -32,20 +32,20 @@ where import Control.Exception (mask_)-import qualified Data.Atomics as Atomics-import Data.Functor (void)-import Data.Primitive.Array (MutableArray)-import qualified Data.Primitive.Array as Array-import GHC.Base (RealWorld)-import qualified Ki+import Data.Primitive.Array qualified as Array+import Ki qualified+import TimerWheel.Internal.Alarm (Alarm (..))+import TimerWheel.Internal.AlarmBuckets (AlarmBuckets, AlarmId)+import TimerWheel.Internal.AlarmBuckets qualified as AlarmBuckets import TimerWheel.Internal.Bucket (Bucket)-import qualified TimerWheel.Internal.Bucket as Bucket+import TimerWheel.Internal.Bucket qualified as Bucket import TimerWheel.Internal.Counter (Counter, decrCounter_, incrCounter, incrCounter_, newCounter, readCounter) import TimerWheel.Internal.Nanoseconds (Nanoseconds (..))-import qualified TimerWheel.Internal.Nanoseconds as Nanoseconds+import TimerWheel.Internal.Nanoseconds qualified as Nanoseconds import TimerWheel.Internal.Prelude+import TimerWheel.Internal.Timer (Timer (..), cancel) import TimerWheel.Internal.Timestamp (Timestamp)-import qualified TimerWheel.Internal.Timestamp as Timestamp+import TimerWheel.Internal.Timestamp qualified as Timestamp -- | A timer wheel is a vector-of-collections-of timers to fire. Timers may be one-shot or recurring, and may be -- scheduled arbitrarily far in the future.@@ -90,11 +90,12 @@ -- | | | 'recurring_' | -- +----------+---------+----------------+ data TimerWheel = TimerWheel- { buckets :: {-# UNPACK #-} !(MutableArray RealWorld (Bucket Timer0)),+ { buckets :: {-# UNPACK #-} !AlarmBuckets, resolution :: {-# UNPACK #-} !Nanoseconds,- numTimers :: {-# UNPACK #-} !Counter,+ -- The number of registered alarms.+ count :: {-# UNPACK #-} !Counter, -- A counter to generate unique ints that identify registered actions, so they can be canceled.- timerIdSupply :: {-# UNPACK #-} !Counter+ supply :: {-# UNPACK #-} !Counter } -- | A timer wheel config.@@ -125,15 +126,15 @@ Config -> -- | IO TimerWheel-create scope (Config spokes0 resolution0) = do+create scope config = do buckets <- Array.newArray spokes Bucket.empty- numTimers <- newCounter- timerIdSupply <- newCounter- Ki.fork_ scope (runTimerReaperThread buckets numTimers resolution)- pure TimerWheel {buckets, numTimers, resolution, timerIdSupply}+ count_ <- newCounter+ supply <- newCounter+ Ki.fork_ scope (runTimerReaperThread buckets resolution)+ pure TimerWheel {buckets, count = count_, resolution, supply} where- spokes = if spokes0 <= 0 then 1024 else spokes0- resolution = Nanoseconds.fromNonNegativeSeconds (if resolution0 <= 0 then 1 else resolution0)+ spokes = if config.spokes <= 0 then 1024 else config.spokes+ resolution = Nanoseconds.fromNonNegativeSeconds (if config.resolution <= 0 then 1 else config.resolution) -- | Perform an action with a timer wheel. with ::@@ -152,8 +153,8 @@ -- -- /O(1)/. count :: TimerWheel -> IO Int-count TimerWheel {numTimers} =- readCounter numTimers+count wheel =+ readCounter wheel.count -- | @register wheel delay action@ registers __@action@__ in __@wheel@__ to fire after __@delay@__ seconds. --@@ -168,19 +169,16 @@ IO () -> -- | The timer IO (Timer Bool)-register TimerWheel {buckets, numTimers, resolution, timerIdSupply} delay action = do+register wheel delay action = do now <- Timestamp.now- let timestamp = now `Timestamp.plus` Nanoseconds.fromSeconds delay- let index = timestampToIndex buckets resolution timestamp- timerId <- incrCounter timerIdSupply- mask_ do- atomicModifyArray buckets index (Bucket.insert timerId timestamp (OneShot1 action))- incrCounter_ numTimers+ let ringsAt = now `Timestamp.plus` Nanoseconds.fromSeconds delay+ alarmId <- incrCounter wheel.supply+ insertAlarm wheel alarmId ringsAt (OneShot (action >> decrCounter_ wheel.count)) coerce @(IO (IO Bool)) @(IO (Timer Bool)) do pure do mask_ do- deleted <- atomicMaybeModifyArray buckets index (Bucket.deleteExpectingHit timerId)- when deleted (decrCounter_ numTimers)+ deleted <- AlarmBuckets.delete wheel.buckets wheel.resolution alarmId ringsAt+ when deleted (decrCounter_ wheel.count) pure deleted -- | Like 'register', but for when you don't intend to cancel the timer.@@ -206,20 +204,16 @@ IO () -> -- | The timer IO (Timer ())-recurring TimerWheel {buckets, numTimers, resolution, timerIdSupply} (Nanoseconds.fromSeconds -> delay) action = do+recurring wheel (Nanoseconds.fromSeconds -> delay) action = do now <- Timestamp.now- let timestamp = now `Timestamp.plus` delay- let index = timestampToIndex buckets resolution timestamp- timerId <- incrCounter timerIdSupply+ alarmId <- incrCounter wheel.supply canceledRef <- newIORef False- mask_ do- atomicModifyArray buckets index (Bucket.insert timerId timestamp (Recurring1 action delay canceledRef))- incrCounter_ numTimers+ insertAlarm wheel alarmId (now `Timestamp.plus` delay) (Recurring action delay canceledRef) coerce @(IO (IO ())) @(IO (Timer ())) do pure do mask_ do writeIORef canceledRef True- decrCounter_ numTimers+ decrCounter_ wheel.count -- | Like 'recurring', but for when you don't intend to cancel the timer. recurring_ ::@@ -229,109 +223,16 @@ -- | The action to fire repeatedly IO () -> IO ()-recurring_ TimerWheel {buckets, numTimers, resolution, timerIdSupply} (Nanoseconds.fromSeconds -> delay) action = do+recurring_ wheel (Nanoseconds.fromSeconds -> delay) action = do now <- Timestamp.now- let timestamp = now `Timestamp.plus` delay- let index = timestampToIndex buckets resolution timestamp- timerId <- incrCounter timerIdSupply- mask_ do- atomicModifyArray buckets index (Bucket.insert timerId timestamp (Recurring1_ action delay))- incrCounter_ numTimers---- | A registered timer, parameterized by the result of attempting to cancel it:------ * A one-shot timer may only be canceled if it has not already fired.--- * A recurring timer can always be canceled.------ __API summary__------ +-------------+----------+--- | Create | Modify |--- +=============+==========+--- | 'register' | 'cancel' |--- +-------------+----------+--- | 'recurring' | |--- +-------------+----------+-newtype Timer a- = Timer (IO a)---- | Cancel a timer.-cancel :: Timer a -> IO a-cancel =- coerce---- `timestampToIndex buckets resolution timestamp` figures out which index `timestamp` corresponds to in `buckets`,--- where each bucket corresponds to `resolution` nanoseconds.------ For example, consider a three-element `buckets` with resolution `1000000000`.------ +--------------------------------------+--- | 1000000000 | 1000000000 | 1000000000 |--- +--------------------------------------+------ Some timestamp like `1053298012387` gets binned to one of the three indices 0, 1, or 2, with quick and easy maffs:------ 1. Figure out which index the timestamp corresponds to, if there were infinitely many:------ 1053298012387 `div` 1000000000 = 1053------ 2. Wrap around per the actual length of the array:------ 1053 `rem` 3 = 0-timestampToIndex :: MutableArray RealWorld bucket -> Nanoseconds -> Timestamp -> Int-timestampToIndex buckets resolution timestamp =- -- This downcast is safe because there are at most `maxBound :: Int` buckets (not that anyone would ever have that- -- many...)- fromIntegral @Word64 @Int- (Timestamp.epoch resolution timestamp `rem` fromIntegral @Int @Word64 (Array.sizeofMutableArray buckets))--data Timer0- = OneShot1 !(IO ())- | Recurring1 !(IO ()) !Nanoseconds !(IORef Bool)- | Recurring1_ !(IO ()) !Nanoseconds--type TimerId =- Int----------------------------------------------------------------------------------------------------------------------------- Atomic operations on arrays--atomicModifyArray :: forall a. MutableArray RealWorld a -> Int -> (a -> a) -> IO ()-atomicModifyArray array index f = do- ticket0 <- Atomics.readArrayElem array index- loop ticket0- where- loop :: Atomics.Ticket a -> IO ()- loop ticket = do- (success, ticket1) <- Atomics.casArrayElem array index ticket (f (Atomics.peekTicket ticket))- if success then pure () else loop ticket1--atomicMaybeModifyArray :: forall a. MutableArray RealWorld a -> Int -> (a -> Maybe a) -> IO Bool-atomicMaybeModifyArray buckets index doDelete = do- ticket0 <- Atomics.readArrayElem buckets index- loop ticket0- where- loop :: Atomics.Ticket a -> IO Bool- loop ticket =- case doDelete (Atomics.peekTicket ticket) of- Nothing -> pure False- Just bucket -> do- (success, ticket1) <- Atomics.casArrayElem buckets index ticket bucket- if success then pure True else loop ticket1+ alarmId <- incrCounter wheel.supply+ insertAlarm wheel alarmId (now `Timestamp.plus` delay) (Recurring_ action delay) -atomicExtractExpiredTimersFromBucket :: MutableArray RealWorld (Bucket Timer0) -> Int -> Timestamp -> IO (Bucket Timer0)-atomicExtractExpiredTimersFromBucket buckets index now = do- ticket0 <- Atomics.readArrayElem buckets index- loop ticket0- where- loop :: Atomics.Ticket (Bucket Timer0) -> IO (Bucket Timer0)- loop ticket = do- let Bucket.Pair expired bucket1 = Bucket.partition now (Atomics.peekTicket ticket)- if Bucket.isEmpty expired- then pure Bucket.empty- else do- (success, ticket1) <- Atomics.casArrayElem buckets index ticket bucket1- if success then pure expired else loop ticket1+insertAlarm :: TimerWheel -> AlarmId -> Timestamp -> Alarm -> IO ()+insertAlarm wheel alarmId ringsAt alarm =+ mask_ do+ incrCounter_ wheel.count+ AlarmBuckets.insert wheel.buckets wheel.resolution alarmId ringsAt alarm ------------------------------------------------------------------------------------------------------------------------ -- Timer reaper thread@@ -426,8 +327,8 @@ -- If the actual time is at or after the next ideal time, that's kind of bad - it means the reaper thread is behind -- schedule. The user's enqueued actions have taken too long, or their wheel resolution is too short. Anyway, it's not -- our problem, our behavior doesn't change per whether we are behind schedule or not.-runTimerReaperThread :: MutableArray RealWorld (Bucket Timer0) -> Counter -> Nanoseconds -> IO void-runTimerReaperThread buckets numTimers resolution = do+runTimerReaperThread :: AlarmBuckets -> Nanoseconds -> IO v+runTimerReaperThread buckets resolution = do -- Sleep until the very first bucket of timers expires -- -- resolution = 100@@ -449,54 +350,46 @@ Nanoseconds.sleep remaining -- Enter the Loop™ let idealTime = now `Timestamp.plus` remaining- theLoop idealTime (timestampToIndex buckets resolution now)+ theLoop idealTime (AlarmBuckets.timestampToIndex buckets resolution now) where -- `index` could be derived from `thisTime`, but it's cheaper to just store it separately and bump by 1 as we go- theLoop :: Timestamp -> Int -> IO void+ theLoop :: Timestamp -> Int -> IO v theLoop !idealTime !index = do- expired2 <- atomicExtractExpiredTimersFromBucket buckets index idealTime- fireTimerBucket expired2- let !nextIdealTime = idealTime `Timestamp.plus` resolution+ expired <- AlarmBuckets.deleteExpiredAt buckets index idealTime+ fireBucket expired+ now <- Timestamp.now+ let !nextIdealTime = idealTime `Timestamp.plus` resolution when (nextIdealTime > now) (Nanoseconds.sleep (nextIdealTime `Timestamp.unsafeMinus` now))+ theLoop nextIdealTime ((index + 1) `rem` Array.sizeofMutableArray buckets) where- fireTimerBucket :: Bucket Timer0 -> IO ()- fireTimerBucket bucket0 =+ fireBucket :: Bucket Alarm -> IO ()+ fireBucket bucket0 = case Bucket.pop bucket0 of Bucket.PopNada -> pure ()- Bucket.PopAlgo timerId timestamp timer bucket1 -> do- expired2 <- fireTimer bucket1 timerId timestamp timer- fireTimerBucket expired2+ Bucket.PopAlgo alarmId ringsAt timer bucket1 -> do+ expired <- fireAlarm bucket1 alarmId ringsAt timer+ fireBucket expired - fireTimer :: Bucket Timer0 -> TimerId -> Timestamp -> Timer0 -> IO (Bucket Timer0)- fireTimer bucket timerId timestamp timer =- case timer of- OneShot1 action -> do+ fireAlarm :: Bucket Alarm -> AlarmId -> Timestamp -> Alarm -> IO (Bucket Alarm)+ fireAlarm bucket alarmId ringsAt alarm =+ case alarm of+ OneShot action -> do action- decrCounter_ numTimers pure bucket- Recurring1 action delay canceledRef ->+ Recurring action delay canceledRef -> readIORef canceledRef >>= \case True -> pure bucket- False -> do- action- scheduleNextOccurrence (timestamp `Timestamp.plus` delay)- Recurring1_ action delay -> do- action- scheduleNextOccurrence (timestamp `Timestamp.plus` delay)+ False -> fireRecurring action delay+ Recurring_ action delay -> fireRecurring action delay where- scheduleNextOccurrence :: Timestamp -> IO (Bucket Timer0)- scheduleNextOccurrence nextOccurrence =- if nextOccurrence < idealTime- then pure $! insertNextOccurrence bucket+ fireRecurring :: IO () -> Nanoseconds -> IO (Bucket Alarm)+ fireRecurring action delay = do+ action+ let ringsAtNext = ringsAt `Timestamp.plus` delay+ if ringsAtNext < idealTime+ then pure $! Bucket.insert alarmId ringsAtNext alarm bucket else do- atomicModifyArray- buckets- (timestampToIndex buckets resolution nextOccurrence)- insertNextOccurrence+ AlarmBuckets.insert buckets resolution alarmId ringsAtNext alarm pure bucket- where- insertNextOccurrence :: Bucket Timer0 -> Bucket Timer0- insertNextOccurrence =- Bucket.insert timerId nextOccurrence timer
+ src/TimerWheel/Internal/Alarm.hs view
@@ -0,0 +1,12 @@+module TimerWheel.Internal.Alarm+ ( Alarm (..),+ )+where++import TimerWheel.Internal.Nanoseconds (Nanoseconds)+import TimerWheel.Internal.Prelude++data Alarm+ = OneShot !(IO ())+ | Recurring !(IO ()) {-# UNPACK #-} !Nanoseconds !(IORef Bool)+ | Recurring_ !(IO ()) {-# UNPACK #-} !Nanoseconds
+ src/TimerWheel/Internal/AlarmBuckets.hs view
@@ -0,0 +1,102 @@+module TimerWheel.Internal.AlarmBuckets+ ( AlarmBuckets,+ AlarmId,+ insert,+ delete,+ deleteExpiredAt,+ timestampToIndex,+ )+where++import Data.Atomics qualified as Atomics+import Data.Primitive.Array (MutableArray)+import Data.Primitive.Array qualified as Array+import GHC.Base (RealWorld)+import TimerWheel.Internal.Alarm (Alarm (..))+import TimerWheel.Internal.Bucket (Bucket)+import TimerWheel.Internal.Bucket qualified as Bucket+import TimerWheel.Internal.Nanoseconds (Nanoseconds)+import TimerWheel.Internal.Prelude+import TimerWheel.Internal.Timestamp (Timestamp)+import TimerWheel.Internal.Timestamp qualified as Timestamp++type AlarmBuckets =+ MutableArray RealWorld (Bucket Alarm)++type AlarmId =+ Int++insert :: AlarmBuckets -> Nanoseconds -> AlarmId -> Timestamp -> Alarm -> IO ()+insert buckets resolution alarmId timestamp alarm = do+ ticket <- Atomics.readArrayElem buckets index+ loop ticket+ where+ loop :: Atomics.Ticket (Bucket Alarm) -> IO ()+ loop ticket = do+ (success, ticket1) <-+ Atomics.casArrayElem+ buckets+ index+ ticket+ (Bucket.insert alarmId timestamp alarm (Atomics.peekTicket ticket))+ if success then pure () else loop ticket1++ index :: Int+ index =+ timestampToIndex buckets resolution timestamp++delete :: AlarmBuckets -> Nanoseconds -> AlarmId -> Timestamp -> IO Bool+delete buckets resolution alarmId timestamp = do+ ticket <- Atomics.readArrayElem buckets index+ loop ticket+ where+ loop :: Atomics.Ticket (Bucket Alarm) -> IO Bool+ loop ticket =+ case Bucket.deleteExpectingHit alarmId (Atomics.peekTicket ticket) of+ Nothing -> pure False+ Just bucket -> do+ (success, ticket1) <- Atomics.casArrayElem buckets index ticket bucket+ if success then pure True else loop ticket1++ index :: Int+ index =+ timestampToIndex buckets resolution timestamp++deleteExpiredAt :: AlarmBuckets -> Int -> Timestamp -> IO (Bucket Alarm)+deleteExpiredAt buckets index now = do+ ticket <- Atomics.readArrayElem buckets index+ loop ticket+ where+ loop :: Atomics.Ticket (Bucket Alarm) -> IO (Bucket Alarm)+ loop ticket = do+ let Bucket.Pair expired bucket1 = Bucket.partition now (Atomics.peekTicket ticket)+ if Bucket.isEmpty expired+ then pure Bucket.empty+ else do+ (success, ticket1) <- Atomics.casArrayElem buckets index ticket bucket1+ if success then pure expired else loop ticket1++-- `timestampToIndex buckets resolution timestamp` figures out which index `timestamp` corresponds to in `buckets`,+-- where each bucket corresponds to `resolution` nanoseconds.+--+-- For example, consider a three-element `buckets` with resolution `1000000000`.+--+-- +--------------------------------------++-- | 1000000000 | 1000000000 | 1000000000 |+-- +--------------------------------------++--+-- Some timestamp like `1053298012387` gets binned to one of the three indices 0, 1, or 2, with quick and easy maffs:+--+-- 1. Figure out which index the timestamp corresponds to, if there were infinitely many:+--+-- 1053298012387 `div` 1000000000 = 1053+--+-- 2. Wrap around per the actual length of the array:+--+-- 1053 `rem` 3 = 0+timestampToIndex :: AlarmBuckets -> Nanoseconds -> Timestamp -> Int+timestampToIndex buckets resolution timestamp =+ -- This downcast is safe because there are at most `maxBound :: Int` buckets (not that anyone would ever have that+ -- many...)+ fromIntegral @Word64 @Int+ (Timestamp.epoch resolution timestamp `rem` fromIntegral @Int @Word64 (Array.sizeofMutableArray buckets))
src/TimerWheel/Internal/Bucket.hs view
@@ -69,13 +69,13 @@ -- a left-bound on the diffbit, that is, the true diffbit can be no left-er than `m`. -- 5. No key in `l` has the `m` bit set -- 6. All keys in `r` have the `m` bit set- Bin {-# UNPACK #-} !TimerId {-# UNPACK #-} !Timestamp !a {-# UNPACK #-} !Mask !(Bucket a) !(Bucket a)- | Tip {-# UNPACK #-} !TimerId {-# UNPACK #-} !Timestamp !a+ Bin {-# UNPACK #-} !AlarmId {-# UNPACK #-} !Timestamp !a {-# UNPACK #-} !Mask !(Bucket a) !(Bucket a)+ | Tip {-# UNPACK #-} !AlarmId {-# UNPACK #-} !Timestamp !a | Nil type Mask = Word64 -type TimerId = Int+type AlarmId = Int -- | An empty bucket. empty :: Bucket a@@ -110,7 +110,7 @@ -- | Insert a new timer into a bucket. -- -- If a timer with the given id is already in the bucket, behavior is undefined.-insert :: forall a. TimerId -> Timestamp -> a -> Bucket a -> Bucket a+insert :: forall a. AlarmId -> Timestamp -> a -> Bucket a -> Bucket a insert i p x bucket = case bucket of Nil -> Tip i p x@@ -141,7 +141,7 @@ linki = link i p x data Pop a- = PopAlgo {-# UNPACK #-} !TimerId {-# UNPACK #-} !Timestamp !a !(Bucket a)+ = PopAlgo {-# UNPACK #-} !AlarmId {-# UNPACK #-} !Timestamp !a !(Bucket a) | PopNada pop :: Bucket a -> Pop a@@ -152,7 +152,7 @@ {-# INLINE pop #-} -- | Delete a timer from a bucket, expecting it to be there.-deleteExpectingHit :: TimerId -> Bucket v -> Maybe (Bucket v)+deleteExpectingHit :: AlarmId -> Bucket v -> Maybe (Bucket v) deleteExpectingHit i = go where@@ -170,12 +170,12 @@ | otherwise -> bin j p x m l <$> go r -- | 'Bin' smart constructor, respecting the invariant that both children can't be 'Nil'.-bin :: TimerId -> Timestamp -> v -> Mask -> Bucket v -> Bucket v -> Bucket v+bin :: AlarmId -> Timestamp -> v -> Mask -> Bucket v -> Bucket v -> Bucket v bin i p x _ Nil Nil = Tip i p x bin i p x m l r = Bin i p x m l r {-# INLINE bin #-} -link :: TimerId -> Timestamp -> v -> TimerId -> Bucket v -> Bucket v -> Bucket v+link :: AlarmId -> Timestamp -> v -> AlarmId -> Bucket v -> Bucket v -> Bucket v link i p x j t u | goleft j m = Bin i p x m t u | otherwise = Bin i p x m u t@@ -272,7 +272,7 @@ -- Bit fiddling -- | Is (or should) this timer be stored on the left of this bin, given its mask?-goleft :: TimerId -> Mask -> Bool+goleft :: AlarmId -> Mask -> Bool goleft i m = i2w i .&. m == 0 {-# INLINE goleft #-}@@ -282,7 +282,7 @@ -- j = JJJJ??????????????????? -- -- prefixNotEqual m i j answers, is IIII not equal to JJJJ?-prefixNotEqual :: Mask -> TimerId -> TimerId -> Bool+prefixNotEqual :: Mask -> AlarmId -> AlarmId -> Bool prefixNotEqual (prefixMask -> e) i j = i2w i .&. e /= i2w j .&. e {-# INLINE prefixNotEqual #-}@@ -297,7 +297,7 @@ onlyHighestBit w = unsafeShiftL 1 (WORD_SIZE_IN_BITS - 1 - countLeadingZeros w) {-# INLINE onlyHighestBit #-} -i2w :: TimerId -> Word64+i2w :: AlarmId -> Word64 i2w = fromIntegral {-# INLINE i2w #-}
src/TimerWheel/Internal/Prelude.hs view
@@ -1,16 +1,17 @@ module TimerWheel.Internal.Prelude ( Seconds,- module X,+ module Reexport, ) where -import Control.Monad as X (when)-import Data.Coerce as X (coerce)+import Control.Monad as Reexport (when)+import Data.Coerce as Reexport (coerce) import Data.Fixed (E9, Fixed)-import Data.IORef as X (IORef, newIORef, readIORef, writeIORef)-import Data.Word as X (Word64)-import GHC.Generics as X (Generic)-import Prelude as X hiding (lookup, null)+import Data.Functor as Reexport (void)+import Data.IORef as Reexport (IORef, newIORef, readIORef, writeIORef)+import Data.Word as Reexport (Word64)+import GHC.Generics as Reexport (Generic)+import Prelude as Reexport hiding (lookup, null) -- | A number of seconds, with nanosecond precision. --
+ src/TimerWheel/Internal/Timer.hs view
@@ -0,0 +1,29 @@+module TimerWheel.Internal.Timer+ ( Timer (..),+ cancel,+ )+where++import TimerWheel.Internal.Prelude++-- | A registered timer, parameterized by the result of attempting to cancel it:+--+-- * A one-shot timer may only be canceled if it has not already fired.+-- * A recurring timer can always be canceled.+--+-- __API summary__+--+-- +-------------+----------++-- | Create | Modify |+-- +=============+==========++-- | 'register' | 'cancel' |+-- +-------------+----------++-- | 'recurring' | |+-- +-------------+----------++newtype Timer a+ = Timer (IO a)++-- | Cancel a timer.+cancel :: Timer a -> IO a+cancel =+ coerce
test/Main.hs view
@@ -124,7 +124,7 @@ class Assert a b where is :: a -> b -> IO () -instance (Eq e, Exception e) => Assert (IO void) (Exn e) where+instance (Eq e, Exception e) => Assert (IO v) (Exn e) where is mx y = do try (void mx) >>= \case Left ex
timer-wheel.cabal view
@@ -1,24 +1,25 @@ cabal-version: 3.0 -author: Mitchell Rosen+author: Mitchell Dalvi Rosen bug-reports: https://github.com/awkward-squad/timer-wheel/issues build-type: Simple category: Data+copyright: (c) 2018-2024 Mitchell Dalvi Rosen, Travis Staton+homepage: https://github.com/awkward-squad/timer-wheel+license-file: LICENSE+license: BSD-3-Clause+maintainer: Mitchell Dalvi Rosen <mitchellwrosen@gmail.com>, Travis Staton <hello@travisstaton.com>+name: timer-wheel+synopsis: A timer wheel+tested-with: GHC == 9.6.5, GHC == 9.8.2, GHC == 9.10.1+version: 1.0.0.1+ description: This library provides a timer wheel data structure for registering one-shot or recurring @IO@ actions to fire after a given amount of time. . It is similar to @TimerManager@ from @GHC.Event@, but supports recurring actions, and can scale to handle many more registered actions.-copyright: (c) 2018-2023 Mitchell Rosen, Travis Staton-homepage: https://github.com/awkward-squad/timer-wheel-license-file: LICENSE-license: BSD-3-Clause-maintainer: Mitchell Rosen <mitchellwrosen@gmail.com>, Travis Staton <hello@travisstaton.com>-name: timer-wheel-synopsis: A timer wheel-tested-with: GHC == 9.4.7, GHC == 9.6.3, GHC == 9.8.1-version: 1.0.0 extra-source-files: CHANGELOG.md@@ -30,28 +31,19 @@ common component build-depends:- base ^>= 4.12 || ^>= 4.13 || ^>= 4.14 || ^>= 4.15 || ^>= 4.16 || ^>= 4.17 || ^>= 4.18 || ^>= 4.19,+ base ^>= 4.16 || ^>= 4.17 || ^>= 4.18 || ^>= 4.19 || ^>= 4.20, default-extensions:- BangPatterns BlockArguments DeriveAnyClass- DeriveGeneric DerivingStrategies DuplicateRecordFields- FlexibleInstances- GeneralizedNewtypeDeriving LambdaCase- MultiParamTypeClasses MultiWayIf NamedFieldPuns- NoImplicitPrelude- NumericUnderscores- OverloadedStrings- ScopedTypeVariables- TupleSections- TypeApplications+ NoFieldSelectors+ OverloadedRecordDot ViewPatterns- default-language: Haskell2010+ default-language: GHC2021 ghc-options: -Weverything -Wno-all-missed-specialisations@@ -83,10 +75,13 @@ TimerWheel hs-source-dirs: src other-modules:+ TimerWheel.Internal.Alarm+ TimerWheel.Internal.AlarmBuckets TimerWheel.Internal.Bucket TimerWheel.Internal.Counter TimerWheel.Internal.Nanoseconds TimerWheel.Internal.Prelude+ TimerWheel.Internal.Timer TimerWheel.Internal.Timestamp test-suite tests@@ -105,7 +100,7 @@ build-depends: ki, random ^>= 1.2,- tasty-bench,+ tasty-bench ^>= 0.3.5, timer-wheel, ghc-options: -fproc-alignment=64 -rtsopts -threaded "-with-rtsopts=-N2 -T -A32m" hs-source-dirs: bench