window-stats: efficient rolling window statistics - #5384
window-stats: efficient rolling window statistics#5384crocodile-dentist wants to merge 8 commits into
Conversation
b83890a to
2a8b584
Compare
2a8b584 to
2f5b77b
Compare
These modules provide a fingertree-backed sliding window with fixed element count. The fingertree backend caches a user-supplied monoidal measure `v` which updates incrementally as elements are added and evicted, which allows for constant time lookup of its value regardless of window size. The Window.Count module exposes a public API, and the private internal implementation is exposed by the Internal.Count module without any guarantees that it stays compatible between any releases. The Measures module exposes some common and practical prebuilt measures.
ab338cb to
21db2bc
Compare
These modules keep samples within a configured duration of the newest sample's timestamp. These variants abstract over the timestamp type via the 'TimeLike' class, which have two implementations defined: - `UTCTime` / `NominalDiffTime` (wall-clock, from `time`) - `Time` / `DiffTime` (monotonic, from `io-classes:si-timers`) Monotonic is preferred when sliding-window correctness must not be perturbed by NTP corrections or wall-clock jumps; wall-clock fits data that already carries `UTCTime` timestamps.
21db2bc to
84ce2ac
Compare
These modules expose sliding windows backed by an approximate t-digest measure, which permit efficient computations of quantiles in bounded memory.
8f8d92d to
25ee034
Compare
These expose some combinators which allow for summarising rolling statistics across a long stream in constant memory.
25ee034 to
a45580f
Compare
| , tmV :: v | ||
| -- ^ user supplied measure |
There was a problem hiding this comment.
Consider making it strict, as all the other fields. A similar tsValue of TimedSample is strict.
| -- | Configured maximum duration of the window. | ||
| windowMaxDuration :: TimedWindow t v a -> Dur t | ||
| windowMaxDuration = twDuration | ||
| {-# INLINE windowMaxDuration #-} |
There was a problem hiding this comment.
Move this below TimedWindow.
Btw, the INLINE pragma is not needed, as GHC will do that automatically for small functions.
| data WelfordMeasure a = WelfordMeasure | ||
| { welfordN :: {-# UNPACK #-} !Int | ||
| , welfordMean :: !a | ||
| , welfordM2 :: !a |
There was a problem hiding this comment.
welfordM2 is variance, isn't it? Could you add a haddock.
| m2 = m2A + m2B + delta * delta | ||
| * fromIntegral nA | ||
| * fromIntegral nB | ||
| / fromIntegral n |
There was a problem hiding this comment.
The indentation is misleading, * should be aligned with the second +.
| -- | ||
| class (Ord t, Ord (Dur t), Num (Dur t)) => TimeLike t where | ||
| -- | The duration type associated with @t@. | ||
| type Dur t = d | d -> t |
| -- for correct behaviour (__note__: this is opposite from 'insertMany' and 'fromFoldable') | ||
| -- \(O(\w log w)\) | ||
| -- | ||
| fromListN :: (TimeLike t, FT.Measured v b, Coercible a b) |
There was a problem hiding this comment.
Do we need this if we have fromFoldable?
The implementation could just be fromListN = fromFoldable and we can let GHC to specialise it for a list.
| -- | ||
| data WindowMeasure v = WindowMeasure | ||
| { wmCount :: {-# UNPACK #-} !Int | ||
| , wmV :: v |
There was a problem hiding this comment.
Why not making it strict in wmV?
| takeUntil p win@Window { windowTree } = win { windowTree = windowTree' } | ||
| where | ||
| windowTree' = FT.takeUntil p windowTree | ||
| !_ = wmV $ FT.measure windowTree' |
There was a problem hiding this comment.
if WindowMeasure would be strict in wmV, then we won't need the strict pattern match here.
|
|
||
| -- | Constructs a window with capacity of @windowMaxSize@ containing a single sample | ||
| -- | ||
| singleton :: (FT.Measured v b, Coercible a b) => Int -> a -> Window v b |
There was a problem hiding this comment.
Why do we need the Coercible in the API, can't the user coerce the data by him self between the types (which is less surprising)?
| FT.EmptyR -> error "impossible" | ||
| prefix FT.:> _ -> prefix | ||
| else ftree | ||
| !_ = FT.measure ftree' |
There was a problem hiding this comment.
This is not needed, FingerTree is strict in the cached measure.
coot
left a comment
There was a problem hiding this comment.
A next batch of modules reviewed, this time the DigestTimeBatched.
| deriving (Generic, NFData, Show) | ||
|
|
||
| instance Foldable (Window v) where | ||
| foldMap f Window { windowTree } = foldMap (f . sampleValue) windowTree |
There was a problem hiding this comment.
Add null, it will be more efficient than the default one.
| instance Foldable (TimedWindow t v) where | ||
| foldMap f TimedWindow { twTree } = | ||
| foldMap (f . tsValue) twTree |
There was a problem hiding this comment.
Add null implementation via FT.null . twTree
| , obDigest = TD.insert a obDigest | ||
| , obCount = succ obCount | ||
| }) | ||
| else (tdwBucket, newBucket) |
There was a problem hiding this comment.
it would be easier to read if we'd return the sealed backed - then a type signature fillBucket :: (Maybe SealedBucket, OpenBucket) would explain what's going on.
| Nothing -> Nothing | ||
| Just bucket -> if obFinish bucket > cutoff | ||
| then tdwBucket win | ||
| else assert (FT.null tdwTree') Nothing |
There was a problem hiding this comment.
🤔 we need to make sure evictBefore is never called when the data from open bucket is not moved to the fingertree while it has entries older than cutoff. For insert to force this invariant, the tdwBucketDuration must be smaller than tdwDuration. This is true unless r passed to empty is positive.
For this to be true it is essential that we have dt < tdwBucketDuration on line 160 above in fillBucket, rather than dt <= tdwBucketDuration.
| empty tdwBucketDuration r = | ||
| TimedDigestWindow | ||
| { tdwDuration = fromIntegral r * tdwBucketDuration | ||
| , tdwBucketDuration | ||
| , tdwBucket = Nothing | ||
| , tdwTree = FT.empty | ||
| , tdwCacheDigest = Nothing | ||
| } |
There was a problem hiding this comment.
| empty tdwBucketDuration r = | |
| TimedDigestWindow | |
| { tdwDuration = fromIntegral r * tdwBucketDuration | |
| , tdwBucketDuration | |
| , tdwBucket = Nothing | |
| , tdwTree = FT.empty | |
| , tdwCacheDigest = Nothing | |
| } | |
| empty _ r | r <= 0 = error "empty: invariant violation, multiplier must be positive" | |
| empty tdwBucketDuration r = | |
| TimedDigestWindow | |
| { tdwDuration = fromIntegral r * tdwBucketDuration | |
| , tdwBucketDuration | |
| , tdwBucket = Nothing | |
| , tdwTree = FT.empty | |
| , tdwCacheDigest = Nothing | |
| } |
See a comment below.
| do | ||
| start <- start' | ||
| finish <- obFinish <$> tdwBucket | ||
| pure $! finish `diffT` start |
There was a problem hiding this comment.
Why not use an applicative:
| do | |
| start <- start' | |
| finish <- obFinish <$> tdwBucket | |
| pure $! finish `diffT` start | |
| diffT <$> (obFinish <$> tdwBucket) | |
| <*> start' |
Description
Efficient sliding-window statistics backed by a finger tree, with a cached user-supplied measure that updates incrementally as the window slides.
This library is a first step towards exposing connection metrics to our applications.
Checklist
Quality
Maintenance
ouroboros-networkproject.