diff --git a/core/src/Streamly/Internal/Data/Time/TimeSpec.hsc b/core/src/Streamly/Internal/Data/Time/TimeSpec.hsc index fcc91f0cee..937651b453 100644 --- a/core/src/Streamly/Internal/Data/Time/TimeSpec.hsc +++ b/core/src/Streamly/Internal/Data/Time/TimeSpec.hsc @@ -1,13 +1,5 @@ {-# OPTIONS_GHC -Wno-identities #-} -#ifndef __GHCJS__ -#include "config.h" -#endif - -#include "Streamly/Internal/Data/Time/Clock/config-clock.h" - -#include "MachDeps.h" - -- | -- Module : Streamly.Internal.Data.Time.TimeSpec -- Copyright : (c) 2019 Composewell Technologies @@ -22,6 +14,14 @@ module Streamly.Internal.Data.Time.TimeSpec ) where +#ifndef __GHCJS__ +#include "config.h" +#endif + +#include "Streamly/Internal/Data/Time/Clock/config-clock.h" + +#include "MachDeps.h" + import Data.Int (Int64) #if (WORD_SIZE_IN_BITS == 32) import Data.Int (Int32) @@ -41,20 +41,23 @@ tenPower9 :: Int64 tenPower9 = 1000000000 ------------------------------------------------------------------------------- --- TimeSpec representation +-- TimeSpec ------------------------------------------------------------------------------- --- A structure storing seconds and nanoseconds as 'Int64' is the simplest and --- fastest way to store practically large quantities of time with efficient --- arithmetic operations. If we store nanoseconds using 'Integer' it can store --- practically unbounded quantities but it may not be as efficient to --- manipulate in performance critical applications. XXX need to measure the --- performance. +-- | 'TimeSpec' can hold time values up to ~292 billion years at nanosecond +-- precision. -- --- | Data type to represent practically large quantities of time efficiently. --- It can represent time up to ~292 billion years at nanosecond resolution. +-- Use 'fromInteger' from the Num instance to create 'TimeSpec' from +-- nanoseconds. Use 'Eq' and 'Ord' instances for comparisons. Use the 'Num' +-- instance to perform arithmetic operations. +-- +-- Note, we assume that 'nsec' is always less than 10^9. Also, when 'TimeSpec' +-- is negative then both 'sec' and 'nsec' must be negative. + +-- XXX Use smart constructors to enforce these assumptions. data TimeSpec = TimeSpec { sec :: {-# UNPACK #-} !Int64 -- ^ seconds + -- This could be Int32 instead but Int64 is as good. , nsec :: {-# UNPACK #-} !Int64 -- ^ nanoseconds } deriving (Eq, Read, Show) @@ -88,11 +91,13 @@ adjustSign t@(TimeSpec s ns) timeSpecToInteger :: TimeSpec -> Integer timeSpecToInteger (TimeSpec s ns) = toInteger $ s * tenPower9 + ns +-- XXX Error on overflow? +-- | Note that the arithmetic operations may overflow silently. instance Num TimeSpec where {-# INLINE (+) #-} t1 + t2 = adjustSign (addWithOverflow t1 t2) - -- XXX will this be more optimal if imlemented without "negate"? + -- XXX will this be more optimal if implemented without "negate"? {-# INLINE (-) #-} t1 - t2 = t1 + negate t2 t1 * t2 = fromInteger $ timeSpecToInteger t1 * timeSpecToInteger t2 @@ -104,7 +109,7 @@ instance Num TimeSpec where {-# INLINE signum #-} signum (TimeSpec s ns) | s == 0 = TimeSpec (signum ns) 0 | otherwise = TimeSpec (signum s) 0 - -- This is fromNanoSecond64 Integer + -- | Convert 'Integer' nanoseconds to 'TimeSpec'. {-# INLINE fromInteger #-} fromInteger nanosec = TimeSpec (fromInteger s) (fromInteger ns) where (s, ns) = nanosec `divMod` toInteger tenPower9 diff --git a/core/src/Streamly/Internal/Data/Time/Units.hs b/core/src/Streamly/Internal/Data/Time/Units.hs index 180fc20976..b5e03ed6a1 100644 --- a/core/src/Streamly/Internal/Data/Time/Units.hs +++ b/core/src/Streamly/Internal/Data/Time/Units.hs @@ -1,39 +1,157 @@ -- | -- Module : Streamly.Internal.Data.Time.Units -- Copyright : (c) 2019 Composewell Technologies --- --- License : BSD3 +-- License : BSD-3-Clause -- Maintainer : streamly@composewell.com -- Stability : pre-release -- Portability : GHC +-- +-- Fast time manipulation. +-- +-- = Fixed Precision 64-bit Unit Types +-- +-- An 'Int64' representation is used for fast time manipulation but reduced +-- time span representation. In high performance code it is recommended to use +-- the 64-bit units if possible. +-- +-- * 'NanoSecond64': 292 years at nanosecond precision. +-- * 'MicroSecond64': 292K years at microsecond precision. +-- * 'MilliSecond64': 292M years at millisecond precision. +-- +-- These units are 'Integral' 'Num' types. We can use 'fromIntegral' to convert +-- any integral type to/from these types. +-- +-- = TimeSpec Type +-- +-- 'TimeSpec' can be slower to manipulate than Int64 but can store upto a +-- duration of ~292 billion years at nanosecond precision. +-- +-- = Absolute and relative times +-- +-- Numbers along with an associated unit (e.g. 'MilliSecond64') are used to +-- represent durations and points in time. Durations are relative but points +-- in time are absolute and defined with respect to some fixed or well known +-- point in time e.g. the Unix epoch (01-Jan-1970). Absolute and relative +-- times are numbers that can be represented and manipulated in the same way as +-- 'Num' types. +-- +-- = RelTime64 +-- +-- Relative time using 64-bit representation, not relative to any specific +-- epoch. Represented using 'NanoSecond64'. 'fromRelTime64' and 'toRelTime64' +-- can be used to convert a time unit to/from 'RelTime64'. Note that a larger +-- unit e.g. 'MicroSecond64' may get truncated if it is larger than 292 years. +-- RelTime64 is also generated by diffing two 'AbsTime' values. +-- +-- RelTime is a 'Num', we can do number arithmetic on RelTime, and use +-- 'fromInteger' to convert an 'Integer' nanoseconds to 'RelTime'. +-- +-- = AbsTime +-- +-- Time measured relative to the POSIX epoch i.e. 01-Jan-1970. Represented +-- using 'TimeSpec'. 'fromAbsTime' and 'toAbsTime' can be used to convert a +-- time unit to/from AbsTime. +-- +-- AbsTime is not a 'Num'. We can use 'diffAbsTime' to diff abstimes to get +-- a 'RelTime'. We can add RelTime to AbsTime to get another AbsTime. +-- +-- = Working with the "time" package +-- +-- AbsTime is essentially the same as 'SystemTime' from the time package. We +-- can use 'SystemTime' to interconvert between time package and this module. + +-- = Alternative Representations +-- +-- Double or Fixed would be a much better representation so that we do not lose +-- information between conversions. However, for faster arithmetic operations +-- we use an 'Int64' here. When we need conservation of values we can use a +-- different system of units with a Fixed precision. +-- +-- = TODO +-- +-- Split the Timespec/TimeUnit in a separate module? +-- Keep *64/TimeUnit64 in this module, remove the 64 suffix because these are +-- common units. +-- Rename TimeUnit to IsTimeSpec, TimeUnit64 to IsTimeUnit. +-- +-- Time (default double precision). Fast Time (64-bit), Wide Time (TimeSpec). +-- Timezone, UTC/Local/System/User-defined. +-- +-- Fast (UTC Time), Wide (Local Time) etc. +-- +-- Units? Can be module specific or wrappers around them. +-- e.g. NanoSecond (Fast (UTC Time)) or NanoSecond Time. +-- +-- Time representations: +-- +-- 'Double' can store large durations at floating precision, larger values +-- would lose precision. +-- +-- 'Integer': type can possibly be used for unbounded fixed precision time. module Streamly.Internal.Data.Time.Units ( - -- * Time Unit Conversions - TimeUnit() - -- , TimeUnitWide() - , TimeUnit64() - - -- * Time Units + -- XXX We could store (days, nanosec) instead of (sec, nanosec) for wider + -- time representations. Or we could use it as 128-bit nanosec + -- representation. We can rename this to NanoSecond128. Is this faster than + -- Integer arithmetic otherwise we could just use Integer type? Try an + -- Integer based implementation and compare benchmarks. Also benchmark + -- integral vs floating (Double) representations. + -- + -- XXX Modules: + -- Time.Unit + -- Time.Unit.TimeSpec (or Int128?) + -- Time.Unit.Int64 + -- Time.Unit.Double + -- Time.AbsTime + -- Time.Posix + -- Time.UTC + -- + -- Each module will implement NanoSecond, MicroSecond, MilliSecond etc. + + -- * TimeSpec + -- | Slower but can represent larger durations (up to 292 billion years at + -- nanosecond precision). + TimeUnit() -- XXX Rename to IsTimeSpec or IsTimeUnit + + -- Name the units based on precision. + -- XXX Rename it to NanoSecondWide or NanoSecWide , TimeSpec(..) + + -- * 64-bit Time Units + -- | 64-bit units are faster but represent shorter durations (up to 292 + -- years at nanosecond precision). Time units can be interconverted using + -- 'fromTimeSpec' and 'toTimeSpec' or using fromNanoSecond64 and + -- toNanoSecond64. + + -- XXX Use TimeSpec instead of Nanosecond64 for conversion if the + -- performance difference is not significant. We can just remove TimeUnit64 + -- and use TimeUnit as the only way to conversion. + , TimeUnit64() -- XXX Rename to IsNanoSecond64 IsTimeUnit64 , NanoSecond64(..) , MicroSecond64(..) , MilliSecond64(..) , showNanoSecond64 - -- * Absolute times (using TimeSpec) + -- * Absolute times + -- | Uses TimeSpec as the underlying representation. , AbsTime(..) , toAbsTime , fromAbsTime - -- * Relative times (using TimeSpec) + -- XXX The name "Duration" may be more intuitive + -- XXX Use separate modules (Time.Duration and Time.Duration64) for RelTime + -- and RelTime64 with the same names. + -- * Durations (long) + -- | Uses 'TimeSpec' as the underlying representation. , RelTime , toRelTime , fromRelTime , diffAbsTime , addToAbsTime - -- * Relative times (using NanoSecond64) + -- * Durations (short) + -- | Uses 'Nanosecond64' as the underlying representation. , RelTime64 , toRelTime64 , fromRelTime64 @@ -83,11 +201,6 @@ tenPower9 = 1000000000 -- NanoSecond Int64 -- ... --- Double or Fixed would be a much better representation so that we do not lose --- information between conversions. However, for faster arithmetic operations --- we use an 'Int64' here. When we need convservation of values we can use a --- different system of units with a Fixed precision. - ------------------------------------------------------------------------------- -- Integral Units ------------------------------------------------------------------------------- @@ -152,6 +265,7 @@ newtype MilliSecond64 = MilliSecond64 Int64 -- performance boost. If not then we can just use Integer nanoseconds and get -- rid of TimeUnitWide. -- +{- -- | A type class for converting between time units using 'Integer' as the -- intermediate and the widest representation with a nanosecond resolution. -- This system of units can represent arbitrarily large times but provides @@ -159,7 +273,6 @@ newtype MilliSecond64 = MilliSecond64 Int64 -- -- NOTE: Converting to and from units may truncate the value depending on the -- original value and the size and resolution of the destination unit. -{- class TimeUnitWide a where toTimeInteger :: a -> Integer fromTimeInteger :: Integer -> a @@ -176,6 +289,22 @@ class TimeUnit a where toTimeSpec :: a -> TimeSpec fromTimeSpec :: TimeSpec -> a +class TimeUnitX a where + -- Return scaling factor with respect to a base unit (e.g. second) If + -- scaling factor is @n@ then the actual value of x units would be x * + -- 10^n base units. + getScaleFactor :: a -> Int + +fromTimeUnit :: forall a b. (Integral a, Num b, TimeUnitX a, TimeUnitX b) => + a -> b +fromTimeUnit a = + fromIntegral (a * 10^(getScaleFactor a - getScaleFactor (undefined :: b))) + +roundTimeUnit :: forall a b. (RealFrac a, Integral b, TimeUnitX a, TimeUnitX b) => + a -> b +roundTimeUnit a = + round (a * 10^(getScaleFactor a - getScaleFactor (undefined :: b))) + -- XXX we can use a fromNanoSecond64 for conversion with overflow check and -- fromNanoSecond64Unsafe for conversion without overflow check. -- @@ -198,11 +327,13 @@ instance TimeUnit TimeSpec where toTimeSpec = id fromTimeSpec = id +-- XXX Remove 64 suffix, regular units should be considered 64 bit. instance TimeUnit NanoSecond64 where {-# INLINE toTimeSpec #-} toTimeSpec (NanoSecond64 t) = TimeSpec s ns where (s, ns) = t `divMod` tenPower9 + -- XXX Check for overflow {-# INLINE fromTimeSpec #-} fromTimeSpec (TimeSpec s ns) = NanoSecond64 $ s * tenPower9 + ns @@ -219,12 +350,14 @@ instance TimeUnit MicroSecond64 where toTimeSpec (MicroSecond64 t) = TimeSpec s (us * tenPower3) where (s, us) = t `divMod` tenPower6 + -- XXX Check for overflow {-# INLINE fromTimeSpec #-} fromTimeSpec (TimeSpec s ns) = -- XXX round ns to nearest microsecond? MicroSecond64 $ s * tenPower6 + (ns `div` tenPower3) instance TimeUnit64 MicroSecond64 where + -- XXX Check for overflow {-# INLINE toNanoSecond64 #-} toNanoSecond64 (MicroSecond64 us) = NanoSecond64 $ us * tenPower3 @@ -237,12 +370,14 @@ instance TimeUnit MilliSecond64 where toTimeSpec (MilliSecond64 t) = TimeSpec s (ms * tenPower6) where (s, ms) = t `divMod` tenPower3 + -- XXX Check for overflow {-# INLINE fromTimeSpec #-} fromTimeSpec (TimeSpec s ns) = -- XXX round ns to nearest millisecond? MilliSecond64 $ s * tenPower3 + (ns `div` tenPower6) instance TimeUnit64 MilliSecond64 where + -- XXX Check for overflow {-# INLINE toNanoSecond64 #-} toNanoSecond64 (MilliSecond64 ms) = NanoSecond64 $ ms * tenPower6 @@ -254,18 +389,32 @@ instance TimeUnit64 MilliSecond64 where -- Absolute time ------------------------------------------------------------------------------- +-- See Data.Fixed +-- +-- XXX Use a 64-bit time unit for faster arithmetic? benchmark. +-- XXX Move AbsTime in the AbsTime module? +-- XXX Export the default Posix module/Clock/Unit from the top level Time module +-- XXX Use separate modules "Time.Posix" and "Time.UTC" for different +-- epochs. +-- XXX Make it "AbsTime a" where a is the time unit. +-- +-- newtype Time = Posix.AbsTime TimeSpec.NanoSecond +-- newtype Time = Posix.AbsTime Double.NanoSecond + -- | Absolute times are relative to a predefined epoch in time. 'AbsTime' -- represents times using 'TimeSpec' which can represent times up to ~292 -- billion years at a nanosecond resolution. newtype AbsTime = AbsTime TimeSpec deriving (Eq, Ord, Show) --- | Convert a 'TimeUnit' to an absolute time. +-- | Convert a 'TimeUnit' representing relative time from the Unix epoch to an +-- absolute time. {-# INLINE_NORMAL toAbsTime #-} toAbsTime :: TimeUnit a => a -> AbsTime toAbsTime = AbsTime . toTimeSpec --- | Convert absolute time to a 'TimeUnit'. +-- | Convert absolute time to a relative 'TimeUnit' representing time relative +-- to the Unix epoch. {-# INLINE_NORMAL fromAbsTime #-} fromAbsTime :: TimeUnit a => AbsTime -> a fromAbsTime (AbsTime t) = fromTimeSpec t @@ -281,13 +430,24 @@ fromAbsTime (AbsTime t) = fromTimeSpec t -- Relative time using NaonoSecond64 as the underlying representation ------------------------------------------------------------------------------- +-- XXX We perhaps do not need a separate RelTime. Use NanoSecond etc. instead +-- of RelTime. They already denote relative time. + +-- For relative times in a stream we can use rollingMap (-). As long as the +-- epoch is fixed we only need to diff the reltime which should be efficient. +-- +-- We can do the same to paths as well. As long as the root is fixed we can +-- diff only the relative components. + -- We use a separate type to represent relative time for safety and speed. -- RelTime has a Num instance, absolute time doesn't. Relative times are -- usually shorter and for our purposes an Int64 nanoseconds can hold close to -- thousand year duration. It is also faster to manipulate. We do not check for -- overflows during manipulations so use it only when you know the time cannot --- be too big. If you need a bigger RelTime representation then use RelTimeBig. +-- be too big. If you need a bigger RelTime representation then use RelTime. +-- This is the same as the DiffTime in time package. +-- -- | Relative times are relative to some arbitrary point of time. Unlike -- 'AbsTime' they are not relative to a predefined epoch. newtype RelTime64 = RelTime64 NanoSecond64 @@ -358,10 +518,12 @@ fromRelTime (RelTime t) = fromTimeSpec t {-# RULES "toRelTime/fromRelTime" forall a. fromRelTime (toRelTime a) = a #-} -- XXX rename to diffAbsTimes? +-- SemigroupR? {-# INLINE diffAbsTime #-} diffAbsTime :: AbsTime -> AbsTime -> RelTime diffAbsTime (AbsTime t1) (AbsTime t2) = RelTime (t1 - t2) +-- SemigroupR? {-# INLINE addToAbsTime #-} addToAbsTime :: AbsTime -> RelTime -> AbsTime addToAbsTime (AbsTime t1) (RelTime t2) = AbsTime $ t1 + t2 diff --git a/src/Streamly/Internal/Data/Stream/IsStream/Transform.hs b/src/Streamly/Internal/Data/Stream/IsStream/Transform.hs index 01376fd1c9..c79cb4f8d6 100644 --- a/src/Streamly/Internal/Data/Stream/IsStream/Transform.hs +++ b/src/Streamly/Internal/Data/Stream/IsStream/Transform.hs @@ -1339,6 +1339,11 @@ indexedR n = fromStreamD . D.indexedR n . toStreamD -- Time Indexing ------------------------------------------------------------------------------- +-- XXX Use the timestamp as (AbsTime, RelTime, a). AbsTime is the time when we +-- started evaluating the stream and RelTime is the time relative to that. +-- When we use only RelTime, AbsTime would be discarded by the compiler so it +-- should not pose any overhead. Have some benchmarks to prove that. + -- Note: The timestamp stream must be the second stream in the zip so that the -- timestamp is generated after generating the stream element and not before. -- If we do not do that then the following example will generate the same