Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions io-classes/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

### Breaking changes

+* Changed `Time` show instance, which now is designed for pasting
+ counterexamples from terminal to an editor.
* Changed `Time` show instance, which now is designed for pasting
* counterexamples from terminal to an editor.

### Non-breaking changes

* Improved performance of `tryReadTBQueueDefault`.
* Added module `Control.Monad.Class.MonadUnique` generalising `Data.Unique`.
* mtl: Added module `Control.Monad.Class.MonadUnique.Trans` providing monad transformer instances for `MonadUnique`.
* Added `roundDiffTimeToMicroseconds` utility function to `si-timers` package (in the `MonadTimer.SI` module).

## 1.8.0.1

Expand Down
24 changes: 24 additions & 0 deletions io-classes/si-timers/src/Control/Monad/Class/MonadTimer/SI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Control.Monad.Class.MonadTimer.SI
-- * Auxiliary functions
, diffTimeToMicrosecondsAsInt
, microsecondsAsIntToDiffTime
, roundDiffTimeToMicroseconds
-- * Re-exports
, DiffTime
, MonadFork
Expand Down Expand Up @@ -54,9 +55,24 @@ diffTimeToMicrosecondsAsInt d =
microsecondsAsIntToDiffTime :: Int -> DiffTime
microsecondsAsIntToDiffTime = (/ 1_000_000) . fromIntegral

-- | Round to microseconds.
--
-- For negative diff times it rounds towards negative infinity, which is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does threadDelay x for x<0 have a different behavior than threadDelay 0?

If not, then this comment seems like a distraction.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

threadDelay doesn't, but timeout does, I am not sure about registerDelay, probably not, I'll check.

-- desirable for `MonadTimer` API.
--
roundDiffTimeToMicroseconds :: DiffTime -> DiffTime
roundDiffTimeToMicroseconds d = fromIntegral usec / 1_000_000
where
-- microseconds
usec :: Integer
usec = diffTimeToPicoseconds d `div` 1_000_000


class ( MonadTimer.MonadDelay m
, MonadMonotonicTime m
) => MonadDelay m where
-- | All instances SHOULD round delays down to the nearest microsecond so the
-- behaviour matches the `IO` instance.
threadDelay :: DiffTime -> m ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of leaving the type as more generous than the canonical implementation's (ie IO), what do you think about instead adding a bespoke newtype?

import Data.Fixed (Micro)

newtype DiffTimeRoundedToMicroseconds = DiffTimeRoundedToMicroseconds Micro
   deriving (...?)

roundDiffTimeToMicroseconds :: DiffTime -> DiffTimeRoundedToMicroseconds

forgetDiffTimeToMicroseconds :: DiffTimeRoundedToMicroseconds -> DiffTime

This would make everything very explicit.

The disadvantage is that people would sometimes be forced to call roundDiffTimeToMicroseconds even when they were certain their DiffTime's internal Pico was already a whole number multiple of 1e6.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we change the API in si-timers and IOSim to use DiffTimeRoundedTOMicroseconds it will have massive consequences in ouroboros-newtork. The cost of changing it might be too big.

The question is, how surprised will one be with threadDelay 0.000_000_1s only to yield the current thread for 0s: it will put it in the back of the scheduler's queue, instead of executing it in another time slot - this could be a bit surprising.

threadDelay negativeDelay doesn't yield a thread, but rounding doesn't change non-negative numbers into negative ones, so no surprises in this case :).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solution conseqences
1. DiffTimeRoundedToMicroseconds breaking API, but no surprises
2. implicit rounding surprises in IOSim, but no surprises when compared to IO
3. the current situation no surprises in IOSim, but different semantics than IO

My pick would be: 1. long-term, 2. or 3. short-term.


-- | Thread delay. This implementation will not over- or underflow.
Expand All @@ -68,6 +84,9 @@ class ( MonadTimer.MonadDelay m
-- For delays smaller than `minBound :: Int` seconds, `minBound :: Int` will be
-- used instead.
--
-- NOTE: since `MonadTimer.threadDelay` uses microsecond precision (as does
-- GHC), so does this instance.
--
instance MonadDelay IO where
threadDelay :: forall m.
MonadDelay m
Expand Down Expand Up @@ -103,6 +122,11 @@ instance MonadDelay IO where
instance MonadDelay m => MonadDelay (ReaderT r m) where
threadDelay = lift . threadDelay

-- | `MonadTimer` API based on SI units (seconds).
--
-- NOTE: all instances SHOULD round delays down to the nearest microsecond so
-- the behaviour matches the `IO` instance.
--
class ( MonadTimer.MonadTimer m
, MonadMonotonicTime m
) => MonadTimer m where
Expand Down
20 changes: 20 additions & 0 deletions io-classes/si-timers/test/Test/MonadTimer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ tests =
prop_diffTimeToMicrosecondsAsIntLeftInverse
, testProperty "diffTimeToMicroseconds right inverse"
prop_diffTimeToMicrosecondsAsIntRightInverse
, testProperty "roundToMicroseconds"
prop_roundDiffTimeToMicroseconds
]

newtype IntDistr = IntDistr Int
Expand Down Expand Up @@ -88,3 +90,21 @@ prop_diffTimeToMicrosecondsAsIntRightInverse (DiffTimeDistr a) =
-> "large"
| otherwise
-> "average"


prop_roundDiffTimeToMicroseconds :: DiffTimeDistr -> Property
prop_roundDiffTimeToMicroseconds (DiffTimeDistr d) =
-- rounded is less or equal to d
--
-- NOTE: this guarantees that if `d < 0` then `d' < 0` which is
-- important for `MonadTimer (IOSim s)` instance.
d' <= d
.&&.
-- difference is less than 1 microsecond
abs (d - d') < 0.000_001
.&&.
-- rounded has no fractional microseconds
case properFraction (d' * 1_000_000) of
(_ :: Integer, f) -> f === 0
where
d' = roundDiffTimeToMicroseconds d
2 changes: 2 additions & 0 deletions io-sim/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
which are based on it: `runSim`, `runSimOrThrow`, or `runSimStrictShutdown`)
with `within` or `discardAfter` from `QuickCheck`. See the test suite how to
use `discardAfter` with `IOSim`.
* Round `si-timers` API (`MonadDelay`, `MonadTimer`) to microsecond to match
`IO` behaviour.

## 1.8.0.1

Expand Down
13 changes: 9 additions & 4 deletions io-sim/src/Control/Monad/IOSim/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -725,7 +725,8 @@ instance MonadDelay (IOSim s) where

instance SI.MonadDelay (IOSim s) where
threadDelay d =
IOSim $ oneShot $ \k -> ThreadDelay d (k ())
IOSim $ oneShot $ \k -> ThreadDelay (SI.roundDiffTimeToMicroseconds d)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Evaluate the rounding calculation strictly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same question applies several other places in this diff.

(k ())

data Timeout s = Timeout !(TVar s TimeoutState) !TimeoutId
-- ^ a timeout
Expand Down Expand Up @@ -765,11 +766,15 @@ instance SI.MonadTimer (IOSim s) where
timeout d action
| d < 0 = Just <$> action
| d == 0 = return Nothing
| otherwise = IOSim $ oneShot $ \k -> StartTimeout d (runIOSim action) k
| otherwise = IOSim $ oneShot $ \k ->
StartTimeout (SI.roundDiffTimeToMicroseconds d)
(runIOSim action)
k

registerDelay d = IOSim $ oneShot $ \k -> RegisterDelay d k
registerDelay d = IOSim $ oneShot $ \k ->
RegisterDelay (SI.roundDiffTimeToMicroseconds d) k
registerDelayCancellable d = do
t <- newTimeout d
t <- newTimeout (SI.roundDiffTimeToMicroseconds d)
return (readTimeout t, cancelTimeout t)

newtype TimeoutException = TimeoutException TimeoutId deriving Eq
Expand Down
Loading