Skip to content

Commit ffab18c

Browse files
committed
Adds random scheduling via threadDelay
1 parent 34a2659 commit ffab18c

File tree

7 files changed

+344
-269
lines changed

7 files changed

+344
-269
lines changed

io-sim/io-sim.cabal

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ library
8585
si-timers ^>=1.3,
8686
time >=1.9.1 && <1.13,
8787
quiet,
88+
random,
8889
QuickCheck,
8990

9091

@@ -113,7 +114,8 @@ test-suite test
113114
tasty,
114115
tasty-quickcheck,
115116
tasty-hunit,
116-
time
117+
time,
118+
random
117119
ghc-options: -fno-ignore-asserts
118120
-rtsopts
119121
if impl(ghc >= 9.8)

io-sim/src/Control/Monad/IOSim.hs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import Test.QuickCheck
131131

132132
import System.IO.Unsafe
133133
import qualified Debug.Trace as Debug
134+
import System.Random (StdGen)
134135

135136

136137
selectTraceEvents
@@ -394,28 +395,28 @@ instance Exception Failure where
394395
, "please report the issue at\n"
395396
, "https://github.com/input-output-hk/io-sim/issues"
396397
]
397-
398+
398399

399400
-- | 'IOSim' is a pure monad.
400401
--
401-
runSim :: forall a. (forall s. IOSim s a) -> Either Failure a
402-
runSim mainAction = traceResult False (runSimTrace mainAction)
402+
runSim :: forall a. StdGen -> (forall s. IOSim s a) -> Either Failure a
403+
runSim stdGen mainAction = traceResult False (runSimTrace stdGen mainAction)
403404

404405
-- | For quick experiments and tests it is often appropriate and convenient to
405406
-- simply throw failures as exceptions.
406407
--
407-
runSimOrThrow :: forall a. (forall s. IOSim s a) -> a
408-
runSimOrThrow mainAction =
409-
case runSim mainAction of
408+
runSimOrThrow :: forall a. StdGen -> (forall s. IOSim s a) -> a
409+
runSimOrThrow stdGen mainAction =
410+
case runSim stdGen mainAction of
410411
Left e -> throw e
411412
Right x -> x
412413

413414
-- | Like 'runSim' but fail when the main thread terminates if there are other
414415
-- threads still running or blocked. If one is trying to follow a strict thread
415416
-- clean-up policy then this helps testing for that.
416417
--
417-
runSimStrictShutdown :: forall a. (forall s. IOSim s a) -> Either Failure a
418-
runSimStrictShutdown mainAction = traceResult True (runSimTrace mainAction)
418+
runSimStrictShutdown :: forall a. StdGen -> (forall s. IOSim s a) -> Either Failure a
419+
runSimStrictShutdown stdGen mainAction = traceResult True (runSimTrace stdGen mainAction)
419420

420421
-- | Fold through the trace and return either a 'Failure' or the simulation
421422
-- result, i.e. the return value of the main thread.
@@ -484,8 +485,8 @@ ppEvents events =
484485

485486
-- | See 'runSimTraceST' below.
486487
--
487-
runSimTrace :: forall a. (forall s. IOSim s a) -> SimTrace a
488-
runSimTrace mainAction = runST (runSimTraceST mainAction)
488+
runSimTrace :: forall a. StdGen -> (forall s. IOSim s a) -> SimTrace a
489+
runSimTrace stdGen mainAction = runST (runSimTraceST stdGen mainAction)
489490

490491
--
491492
-- IOSimPOR

io-sim/src/Control/Monad/IOSim/Internal.hs

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,13 @@ import Control.Monad.Class.MonadSTM hiding (STM)
7676
import Control.Monad.Class.MonadSTM.Internal (TMVarDefault (TMVar))
7777
import Control.Monad.Class.MonadThrow hiding (getMaskingState)
7878
import Control.Monad.Class.MonadTime
79-
import Control.Monad.Class.MonadTimer.SI (TimeoutState (..))
79+
import Control.Monad.Class.MonadTimer.SI (TimeoutState (..), DiffTime, diffTimeToMicrosecondsAsInt, microsecondsAsIntToDiffTime)
8080

8181
import Control.Monad.IOSim.InternalTypes
8282
import Control.Monad.IOSim.Types hiding (SimEvent (SimPOREvent),
8383
Trace (SimPORTrace))
8484
import 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} =
189192
timeSinceEpoch :: Time -> NominalDiffTime
190193
timeSinceEpoch (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

Comments
 (0)