@@ -76,12 +76,13 @@ import Control.Monad.Class.MonadSTM hiding (STM)
7676import Control.Monad.Class.MonadSTM.Internal (TMVarDefault (TMVar ))
7777import Control.Monad.Class.MonadThrow hiding (getMaskingState )
7878import Control.Monad.Class.MonadTime
79- import Control.Monad.Class.MonadTimer.SI (TimeoutState (.. ))
79+ import Control.Monad.Class.MonadTimer.SI (TimeoutState (.. ), DiffTime , diffTimeToMicrosecondsAsInt , microsecondsAsIntToDiffTime )
8080
8181import Control.Monad.IOSim.InternalTypes
8282import Control.Monad.IOSim.Types hiding (SimEvent (SimPOREvent ),
8383 Trace (SimPORTrace ))
8484import Control.Monad.IOSim.Types (SimEvent )
85+ import System.Random (StdGen , randomR , split )
8586
8687--
8788-- Simulation interpreter
@@ -150,19 +151,21 @@ data SimState s a = SimState {
150151 -- | list of clocks
151152 clocks :: ! (Map ClockId UTCTime ),
152153 nextVid :: ! TVarId , -- ^ next unused 'TVarId'
153- nextTmid :: ! TimeoutId -- ^ next unused 'TimeoutId'
154+ nextTmid :: ! TimeoutId , -- ^ next unused 'TimeoutId'
155+ stdGen :: ! StdGen
154156 }
155157
156- initialState :: SimState s a
157- initialState =
158+ initialState :: StdGen -> SimState s a
159+ initialState stdGen =
158160 SimState {
159161 runqueue = mempty ,
160162 threads = Map. empty,
161163 curTime = Time 0 ,
162164 timers = PSQ. empty,
163165 clocks = Map. singleton (ClockId [] ) epoch1970,
164166 nextVid = TVarId 0 ,
165- nextTmid = TimeoutId 0
167+ nextTmid = TimeoutId 0 ,
168+ stdGen = stdGen
166169 }
167170 where
168171 epoch1970 = UTCTime (fromGregorian 1970 1 1 ) 0
@@ -189,6 +192,42 @@ invariant Nothing SimState{runqueue,threads,clocks} =
189192timeSinceEpoch :: Time -> NominalDiffTime
190193timeSinceEpoch (Time t) = fromRational (toRational t)
191194
195+ -- | This function receives a delay and adds jitter to it. The amount of
196+ -- jitter added is proportional to how large the delay is so to not greatly
197+ -- affect the indended behaviour of the function that calls it.
198+ --
199+ -- This function is used in order to introduce random delays between
200+ -- concurrent threads so that different thread schedulings might be found.
201+ --
202+ -- This approach is nice because, since time is perfect (due to infinite
203+ -- processing power of IOSim), IOSim will be able to introduce slight delays
204+ -- that might lead to threads being scheduled differently.
205+ --
206+ -- Note that this only enables IOSim to explore different thread schedules for
207+ -- concurrent threads blocked on 'threadDelay'. For threads blocked on STM
208+ -- IOSim employs a way to awake threads in a pseudo random way.
209+ --
210+ -- Also note that it is safe to add jitter to 'threadDelay' because we only
211+ -- have to guarantee that the thread is not woken up earlier than the delay
212+ -- specified.
213+ --
214+ jitterDelay :: StdGen -> DiffTime -> DiffTime
215+ jitterDelay stdGen d =
216+ let -- Convert delay from DiffTime to picoseconds
217+ delayInMicrosecondsAsInt = diffTimeToMicrosecondsAsInt d
218+
219+ -- Define the maximum jitter as a percentage of the delay
220+ -- For example, 10% of the delay
221+ maxJitter = delayInMicrosecondsAsInt `div` 10
222+
223+ -- Generate a random jitter value within the range
224+ (jitterInMicrosecondsAsInt, _) = randomR (0 , maxJitter) stdGen
225+
226+ -- Convert jitter back to DiffTime
227+ jitter = microsecondsAsIntToDiffTime jitterInMicrosecondsAsInt
228+
229+ in -- Add jitter to the original delay
230+ d + jitter
192231
193232-- | Schedule / run a thread.
194233--
@@ -205,7 +244,8 @@ schedule !thread@Thread{
205244 timers,
206245 clocks,
207246 nextVid, nextTmid,
208- curTime = time
247+ curTime = time,
248+ stdGen
209249 } =
210250 invariant (Just thread) simstate $
211251 case action of
@@ -390,12 +430,15 @@ schedule !thread@Thread{
390430 ! tvar <- execNewTVar nextVid
391431 (Just $! " <<timeout " ++ show (unTimeoutId nextTmid) ++ " >>" )
392432 False
393- let ! expiry = d `addTime` time
433+ let ! expiry = jitterDelay stdGen d `addTime` time
394434 ! timers' = PSQ. insert nextTmid expiry (TimerRegisterDelay tvar) timers
395435 ! thread' = thread { threadControl = ThreadControl (k tvar) ctl }
436+ (_, ! stdGen') = split stdGen
396437 trace <- schedule thread' simstate { timers = timers'
397438 , nextVid = succ nextVid
398- , nextTmid = succ nextTmid }
439+ , nextTmid = succ nextTmid
440+ , stdGen = stdGen'
441+ }
399442 return (SimTrace time tid tlbl
400443 (EventRegisterDelayCreated nextTmid nextVid expiry) trace)
401444
@@ -409,11 +452,14 @@ schedule !thread@Thread{
409452 trace)
410453
411454 ThreadDelay d k -> do
412- let ! expiry = d `addTime` time
455+ let ! expiry = jitterDelay stdGen d `addTime` time
413456 ! timers' = PSQ. insert nextTmid expiry (TimerThreadDelay tid nextTmid) timers
414457 ! thread' = thread { threadControl = ThreadControl (Return () ) (DelayFrame nextTmid k ctl) }
458+ (_, ! stdGen') = split stdGen
415459 ! trace <- deschedule (Blocked BlockedOnDelay ) thread' simstate { timers = timers'
416- , nextTmid = succ nextTmid }
460+ , nextTmid = succ nextTmid
461+ , stdGen = stdGen'
462+ }
417463 return (SimTrace time tid tlbl (EventThreadDelay nextTmid expiry) trace)
418464
419465 -- we treat negative timers as cancelled ones; for the record we put
@@ -432,13 +478,16 @@ schedule !thread@Thread{
432478 ! tvar <- execNewTVar nextVid
433479 (Just $! " <<timeout-state " ++ show (unTimeoutId nextTmid) ++ " >>" )
434480 TimeoutPending
435- let ! expiry = d `addTime` time
481+ let ! expiry = jitterDelay stdGen d `addTime` time
436482 ! t = Timeout tvar nextTmid
437483 ! timers' = PSQ. insert nextTmid expiry (Timer tvar) timers
438484 ! thread' = thread { threadControl = ThreadControl (k t) ctl }
485+ (_, ! stdGen') = split stdGen
439486 trace <- schedule thread' simstate { timers = timers'
440487 , nextVid = succ nextVid
441- , nextTmid = succ nextTmid }
488+ , nextTmid = succ nextTmid
489+ , stdGen = stdGen'
490+ }
442491 return (SimTrace time tid tlbl (EventTimerCreated nextTmid nextVid expiry) trace)
443492
444493 CancelTimeout (Timeout tvar tmid) k -> do
@@ -861,9 +910,9 @@ forkTimeoutInterruptThreads timeoutExpired simState =
861910 where
862911 -- we launch a thread responsible for throwing an AsyncCancelled exception
863912 -- to the thread which timeout expired
864- throwToThread :: [(Thread s a , TMVar (IOSim s ) IOSimThreadId )]
913+ throwToThread :: [(Thread s a , TMVar (IOSim s ) IOSimThreadId )]
865914
866- (simState', throwToThread) = List. mapAccumR fn simState timeoutExpired
915+ (simState', throwToThread) = List. mapAccumR fn simState timeoutExpired
867916 where
868917 fn :: SimState s a
869918 -> (IOSimThreadId , TimeoutId , TMVar (IOSim s ) IOSimThreadId )
@@ -997,8 +1046,8 @@ lookupThreadLabel tid threads = join (threadLabel <$> Map.lookup tid threads)
9971046-- computation with 'Control.Monad.IOSim.traceEvents'. A slightly more
9981047-- convenient way is exposed by 'Control.Monad.IOSim.runSimTrace'.
9991048--
1000- runSimTraceST :: forall s a . IOSim s a -> ST s (SimTrace a )
1001- runSimTraceST mainAction = schedule mainThread initialState
1049+ runSimTraceST :: forall s a . StdGen -> IOSim s a -> ST s (SimTrace a )
1050+ runSimTraceST stdGen mainAction = schedule mainThread ( initialState stdGen)
10021051 where
10031052 mainThread =
10041053 Thread {
0 commit comments